deepline 0.3.332 → 0.3.334

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 (122) hide show
  1. package/dist/bundling-sources/sdk/src/play.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +200 -43
  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 +8 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +16 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +30 -9
  8. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -7
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +14 -14
  11. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +12 -5
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +6 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +95 -24
  14. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +132 -37
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +13 -5
  16. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +1 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +87 -3
  18. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +7 -0
  19. package/dist/cli/{auth-V4IWMXTG.mjs → auth-PAEBUPRS.mjs} +4 -4
  20. package/dist/cli/auth-WR3XFOHN.js +31 -0
  21. package/dist/cli/{billing-GYW4BCPG.mjs → billing-QGQJUM5G.mjs} +4 -4
  22. package/dist/cli/{billing-BW37QW4H.js → billing-V746QMQC.js} +71 -71
  23. package/dist/cli/{chunk-GOH5BRGN.js → chunk-6466HVYR.js} +26 -26
  24. package/dist/cli/{chunk-JHF3GWKX.js → chunk-6MVMTDL7.js} +4 -4
  25. package/dist/cli/{chunk-TTJ7KELW.js → chunk-7XXNMV6G.js} +45 -45
  26. package/dist/cli/{chunk-WQI642QX.mjs → chunk-A7S4PRK2.mjs} +1 -1
  27. package/dist/cli/{chunk-HKQMCTEE.mjs → chunk-EJLUQARV.mjs} +4 -4
  28. package/dist/cli/{chunk-SBPJWA6Y.mjs → chunk-EJVAWHQ7.mjs} +6 -6
  29. package/dist/cli/{chunk-GDBJYM5N.js → chunk-FLMBKUHB.js} +60 -60
  30. package/dist/cli/{chunk-U2B5KDNH.js → chunk-GNHI4WZC.js} +15 -15
  31. package/dist/cli/{chunk-KYQPEIKA.mjs → chunk-GUMAWNWZ.mjs} +5 -5
  32. package/dist/cli/{chunk-CYLZE6RJ.js → chunk-GX2YGFQV.js} +1 -1
  33. package/dist/cli/{chunk-X6MY7BC2.js → chunk-HIPRSI5H.js} +225 -225
  34. package/dist/cli/{chunk-AF6XLGZT.js → chunk-NLHRXDIT.js} +2 -2
  35. package/dist/cli/{chunk-UMKN2BZK.mjs → chunk-O564PHDU.mjs} +1 -1
  36. package/dist/cli/{chunk-MXJVBJYC.mjs → chunk-O64RSXDZ.mjs} +2 -2
  37. package/dist/cli/{chunk-3E7P62NA.mjs → chunk-P3ZX3NSE.mjs} +1 -1
  38. package/dist/cli/{chunk-ERPCUHOQ.js → chunk-R6O3YVIG.js} +8 -8
  39. package/dist/cli/{chunk-PK4EE6T3.js → chunk-SABEM6FL.js} +48 -48
  40. package/dist/cli/{chunk-J5Z5BQGE.mjs → chunk-SBEWKGUA.mjs} +1 -1
  41. package/dist/cli/{chunk-4RLN2DHZ.mjs → chunk-TT2CLWMK.mjs} +1 -1
  42. package/dist/cli/{chunk-B6R2V3TH.mjs → chunk-TVIWEQ4V.mjs} +3 -3
  43. package/dist/cli/{chunk-MSQK2FLJ.mjs → chunk-U724V33M.mjs} +2 -2
  44. package/dist/cli/{chunk-T5TGWH4J.js → chunk-UUB3FTKZ.js} +6 -6
  45. package/dist/cli/{chunk-LAJ56FLL.mjs → chunk-VB7TNIUB.mjs} +1 -1
  46. package/dist/cli/{chunk-ZZWCK6F3.js → chunk-XXZSLV7B.js} +6 -6
  47. package/dist/cli/{chunk-5FHGKEAD.mjs → chunk-Y2VN54ME.mjs} +2 -2
  48. package/dist/cli/{chunk-XON4H3CO.js → chunk-Z6VHC643.js} +9 -9
  49. package/dist/cli/client-6OBIO6GC.js +20 -0
  50. package/dist/cli/{client-TBCDCJ7T.mjs → client-JIZWAGVW.mjs} +3 -3
  51. package/dist/cli/{csv-O3NEPSU3.mjs → csv-7DCY7Z6R.mjs} +3 -3
  52. package/dist/cli/{csv-EFQ7UL3M.js → csv-KAFKXAFU.js} +12 -12
  53. package/dist/cli/{db-2FQ3T4LT.js → db-5LJPO2VA.js} +29 -29
  54. package/dist/cli/{db-RUWCFINP.mjs → db-KJBWUFPL.mjs} +4 -4
  55. package/dist/cli/{describe-4KSVRYPM.mjs → describe-CPF4KDKZ.mjs} +8 -8
  56. package/dist/cli/{describe-YJFDN54P.js → describe-VJEEVOH6.js} +15 -15
  57. package/dist/cli/{executions-MG63LYMV.mjs → executions-LJN5QOK7.mjs} +4 -4
  58. package/dist/cli/{executions-O7IIWVRV.js → executions-LPVCV5SC.js} +6 -6
  59. package/dist/cli/{feedback-WMYRDK2J.mjs → feedback-3HYS6ECC.mjs} +3 -3
  60. package/dist/cli/{feedback-SR2X2H4N.js → feedback-K26WP7HJ.js} +6 -6
  61. package/dist/cli/{git-NIGE3SIJ.mjs → git-U4VBLXSS.mjs} +4 -4
  62. package/dist/cli/git-VMQTXKJQ.js +23 -0
  63. package/dist/cli/index.js +79 -79
  64. package/dist/cli/index.mjs +31 -31
  65. package/dist/cli/{legacy-noops-TXSBNOG6.js → legacy-noops-32YL3VF4.js} +7 -7
  66. package/dist/cli/{legacy-noops-YXYYCDSR.mjs → legacy-noops-ORHX2FMA.mjs} +4 -4
  67. package/dist/cli/{monitors-QYAGRL6V.mjs → monitors-5JJBEJS7.mjs} +4 -4
  68. package/dist/cli/{monitors-ZZUQMDUA.js → monitors-NFE4ICED.js} +85 -85
  69. package/dist/cli/{org-QQSCJJMM.js → org-7CJS35TF.js} +60 -60
  70. package/dist/cli/{org-6NUUN2P7.mjs → org-RJYSDJ3W.mjs} +4 -4
  71. package/dist/cli/play-FUZYMPHF.js +153 -0
  72. package/dist/cli/{play-3G2A53MD.mjs → play-H2QUTJR2.mjs} +7 -7
  73. package/dist/cli/{providers-IGD3ZYTA.mjs → providers-TEVRE4Q6.mjs} +4 -4
  74. package/dist/cli/{providers-AASOAAWT.js → providers-VUKRZWZY.js} +7 -7
  75. package/dist/cli/{quickstart-RRCLOSHQ.mjs → quickstart-2MIHVSZO.mjs} +3 -3
  76. package/dist/cli/{quickstart-NDUT5BVF.js → quickstart-36OGYQ2P.js} +5 -5
  77. package/dist/cli/search-DFSHYVZ5.js +26 -0
  78. package/dist/cli/{search-37MO75IS.mjs → search-IRRQSN6R.mjs} +5 -5
  79. package/dist/cli/{secrets-3OTOG64E.mjs → secrets-3W6OVDVE.mjs} +4 -4
  80. package/dist/cli/{secrets-QXY6DCJA.js → secrets-K67ZKZZM.js} +13 -13
  81. package/dist/cli/{sessions-FML35Y64.mjs → sessions-AOMOV4HG.mjs} +4 -4
  82. package/dist/cli/{sessions-3P6DOM3H.js → sessions-SLQ2QFLK.js} +5 -5
  83. package/dist/cli/{settings-SD6MDI67.js → settings-2X7H6O4I.js} +74 -74
  84. package/dist/cli/{settings-MWFWIGHW.mjs → settings-L4IFYF5D.mjs} +7 -7
  85. package/dist/cli/{setup-HY35HR26.js → setup-2RY5APQS.js} +41 -41
  86. package/dist/cli/{setup-HDYPYPFU.mjs → setup-CZYQTZ2K.mjs} +6 -6
  87. package/dist/cli/{skills-TGEDPYTP.mjs → skills-D2QQH5S7.mjs} +4 -4
  88. package/dist/cli/skills-RIFYRYWL.js +28 -0
  89. package/dist/cli/tools-M7LTH6ZT.js +40 -0
  90. package/dist/cli/{tools-VBARDH32.mjs → tools-TYIEROJJ.mjs} +6 -6
  91. package/dist/cli/{update-OIDLOWGB.mjs → update-7YCSUWFP.mjs} +6 -6
  92. package/dist/cli/update-HC62SVSQ.js +28 -0
  93. package/dist/cli/{workflow-K2SJUC5Z.js → workflow-42IFLYPN.js} +15 -15
  94. package/dist/cli/{workflow-ACHWLHPT.mjs → workflow-GGRUCLCI.mjs} +5 -5
  95. package/dist/{client-l-IoLh8Z.d.ts → client-DkoxT4wF.d.ts} +1 -1
  96. package/dist/{client-D5XOQKsA.d.mts → client-PDAfbylh.d.mts} +1 -1
  97. package/dist/{compiler-manifest-82q4jtfQ.d.ts → compiler-manifest-CButyeZ8.d.mts} +7 -0
  98. package/dist/{compiler-manifest-82q4jtfQ.d.mts → compiler-manifest-CButyeZ8.d.ts} +7 -0
  99. package/dist/{index-BPbW2XHF.d.mts → index-BMsNUoL5.d.mts} +1 -1
  100. package/dist/{index-k0rquHIm.d.ts → index-LDRzwo09.d.ts} +1 -1
  101. package/dist/index.d.mts +6 -6
  102. package/dist/index.d.ts +6 -6
  103. package/dist/index.js +6 -6
  104. package/dist/index.mjs +3 -3
  105. package/dist/install-integrity.json +80 -80
  106. package/dist/plays/bundle-play-file.d.mts +3 -3
  107. package/dist/plays/bundle-play-file.d.ts +3 -3
  108. package/dist/plays/run-request.d.mts +2 -2
  109. package/dist/plays/run-request.d.ts +2 -2
  110. package/dist/release.d.mts +1 -1
  111. package/dist/release.d.ts +1 -1
  112. package/dist/release.js +2 -2
  113. package/dist/release.mjs +1 -1
  114. package/package.json +1 -1
  115. package/dist/cli/auth-6SVPO5HS.js +0 -31
  116. package/dist/cli/client-BPQKKACG.js +0 -20
  117. package/dist/cli/git-ZZU4LXWX.js +0 -23
  118. package/dist/cli/play-LLC44DQW.js +0 -153
  119. package/dist/cli/search-JWUKJSNY.js +0 -26
  120. package/dist/cli/skills-T3F4A4BQ.js +0 -28
  121. package/dist/cli/tools-PXC5VIRU.js +0 -40
  122. package/dist/cli/update-PIT4RK74.js +0 -28
@@ -477,7 +477,7 @@ export type CsvOptions = PlayAuthoringCsvOptions;
477
477
  * // Emit a log line (visible in `play tail`)
478
478
  * ctx.log(`Loaded ${await leads.count()} leads`);
479
479
  *
480
- * // Pause execution
480
+ * // Wait at least one second before continuing.
481
481
  * await ctx.sleep(1000);
482
482
  *
483
483
  * // Access submitted input through the handler's second argument.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.332',
203
+ version: '0.3.334',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -222,7 +222,8 @@ import {
222
222
  classifyToolExecuteHttpFailure,
223
223
  createToolExecuteHttpFailureAttemptTracker,
224
224
  parseToolExecuteRetryAfterMs,
225
- parseToolExecuteRetryAtMs,
225
+ parseToolExecuteProviderRetryAtMs,
226
+ parseToolExecuteExactProviderRetryAtMs,
226
227
  parseToolExecuteAuthScopeChangedError,
227
228
  ToolExecuteAuthScopeChangedError,
228
229
  type ToolExecuteHttpFailureOutcome,
@@ -653,6 +654,7 @@ const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
653
654
  export const TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS = 30_000;
654
655
  const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
655
656
  const TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS = 5_000;
657
+ const AUTHORED_SLEEP_RESIDENT_LIMIT_MS = 10_000;
656
658
  const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
657
659
  // Every runtime-API tool fetch needs a client-side deadline. Without one, a
658
660
  // stalled execute request (e.g. a credential-less call whose gateway connection
@@ -2928,6 +2930,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2928
2930
  */
2929
2931
  private readonly persistenceLatch: RuntimePersistenceLatch =
2930
2932
  createRuntimePersistenceLatch();
2933
+ private sleepMetadataPersistenceFailed = false;
2931
2934
  /**
2932
2935
  * The single source of every concurrency, budget, and pacing decision for this
2933
2936
  * run-attempt. Tool/play/row slots are blocking semaphores; budgets are charged
@@ -3153,6 +3156,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3153
3156
  */
3154
3157
  assertNoPendingDurableSuspension(): void {
3155
3158
  this.#options.executionSignal?.throwIfAborted();
3159
+ if (this.sleepMetadataPersistenceFailed) {
3160
+ throw new RuntimePersistenceCircuitOpenError(this.persistenceLatch);
3161
+ }
3156
3162
  this.assertNoSlackHitlTimeoutFailure();
3157
3163
  this.assertNoRowProviderBackpressure();
3158
3164
  if (this.pendingDurableSuspension) {
@@ -6234,7 +6240,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6234
6240
  runningReceiptWaitMaxAttempts?: number;
6235
6241
  runningReceiptWaitDelayMs?: number;
6236
6242
  reclaimRunning?: boolean;
6237
- markSkipped?: (output: T) => Promise<void> | void;
6243
+ markSkipped?: (
6244
+ output: T,
6245
+ receipt: RuntimeStepReceipt,
6246
+ ) => Promise<void> | void;
6238
6247
  onRecovered?: (
6239
6248
  output: T,
6240
6249
  receipt: RuntimeStepReceipt,
@@ -14580,6 +14589,111 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14580
14589
  this.suspendDurably({ kind: 'sleep', boundaryId, delayMs });
14581
14590
  }
14582
14591
 
14592
+ private sleepMetadataReceiptKey(
14593
+ boundaryId: string,
14594
+ intent: 'wake' | 'count',
14595
+ ): string {
14596
+ return durableCtxKey({
14597
+ orgId: this.#options.orgId,
14598
+ playId: this.currentExecutionScope.receipt.namespace,
14599
+ operation: 'step',
14600
+ id: `__deepline_sleep_${intent}__`,
14601
+ semanticKey: stableStringify({
14602
+ runId: this.currentReceiptOwnerRunId,
14603
+ boundaryId,
14604
+ }),
14605
+ });
14606
+ }
14607
+
14608
+ private async readStepSleepCount(boundaryId: string): Promise<number | null> {
14609
+ try {
14610
+ const receipt = await this.getRuntimeStepReceipt(
14611
+ this.sleepMetadataReceiptKey(boundaryId, 'count'),
14612
+ );
14613
+ if (receipt?.status !== 'completed') return null;
14614
+ const output = receipt.output;
14615
+ const sleepCount =
14616
+ output && typeof output === 'object' && 'sleepCount' in output
14617
+ ? output.sleepCount
14618
+ : undefined;
14619
+ if (
14620
+ typeof sleepCount !== 'number' ||
14621
+ !Number.isSafeInteger(sleepCount) ||
14622
+ sleepCount < 0
14623
+ ) {
14624
+ throw new Error(`Durable step ${boundaryId} has no valid sleep count.`);
14625
+ }
14626
+ return sleepCount;
14627
+ } catch (error) {
14628
+ this.sleepMetadataPersistenceFailed = true;
14629
+ tripRuntimePersistenceLatch(this.persistenceLatch, error);
14630
+ throw error;
14631
+ }
14632
+ }
14633
+
14634
+ private async persistStepSleepCount(
14635
+ boundaryId: string,
14636
+ sleepCount: number,
14637
+ ): Promise<void> {
14638
+ try {
14639
+ await this.completeRuntimeStepReceipt(
14640
+ this.sleepMetadataReceiptKey(boundaryId, 'count'),
14641
+ this.currentReceiptOwnerRunId,
14642
+ { sleepCount },
14643
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
14644
+ );
14645
+ if ((await this.readStepSleepCount(boundaryId)) === null) {
14646
+ throw new Error(
14647
+ `Durable step ${boundaryId} did not persist its sleep count.`,
14648
+ );
14649
+ }
14650
+ } catch (error) {
14651
+ this.sleepMetadataPersistenceFailed = true;
14652
+ tripRuntimePersistenceLatch(this.persistenceLatch, error);
14653
+ throw error;
14654
+ }
14655
+ }
14656
+
14657
+ private async resolveCanonicalSleepWakeAtMs(
14658
+ boundaryId: string,
14659
+ delayMs: number,
14660
+ ): Promise<number> {
14661
+ const key = this.sleepMetadataReceiptKey(boundaryId, 'wake');
14662
+ const proposedWakeAtMs = Date.now() + delayMs;
14663
+ try {
14664
+ let receipt = await this.getRuntimeStepReceipt(key);
14665
+ if (receipt?.status !== 'completed') {
14666
+ await this.completeRuntimeStepReceipt(
14667
+ key,
14668
+ this.currentReceiptOwnerRunId,
14669
+ { wakeAtMs: proposedWakeAtMs },
14670
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
14671
+ );
14672
+ receipt = await this.getRuntimeStepReceipt(key);
14673
+ }
14674
+ const output = receipt?.output;
14675
+ const wakeAtMs =
14676
+ output && typeof output === 'object' && 'wakeAtMs' in output
14677
+ ? output.wakeAtMs
14678
+ : undefined;
14679
+ if (
14680
+ receipt?.status !== 'completed' ||
14681
+ typeof wakeAtMs !== 'number' ||
14682
+ !Number.isSafeInteger(wakeAtMs) ||
14683
+ wakeAtMs < 0
14684
+ ) {
14685
+ throw new Error(
14686
+ `Durable sleep intent ${boundaryId} has no valid wake timestamp.`,
14687
+ );
14688
+ }
14689
+ return wakeAtMs;
14690
+ } catch (error) {
14691
+ this.sleepMetadataPersistenceFailed = true;
14692
+ tripRuntimePersistenceLatch(this.persistenceLatch, error);
14693
+ throw error;
14694
+ }
14695
+ }
14696
+
14583
14697
  async sleep(ms: number): Promise<void> {
14584
14698
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14585
14699
  this.assertNoPendingDurableSuspension();
@@ -14653,17 +14767,49 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14653
14767
  if (existing?.kind === 'sleep' && existing.completedAt !== undefined) {
14654
14768
  return;
14655
14769
  }
14770
+ const wakeAtMs =
14771
+ existing?.kind === 'sleep' && existing.wakeAtMs !== undefined
14772
+ ? existing.wakeAtMs
14773
+ : this.durableDirectToolResultsBackedByReceipts
14774
+ ? await this.resolveCanonicalSleepWakeAtMs(boundaryId, delayMs)
14775
+ : undefined;
14776
+ this.assertNoPendingDurableSuspension();
14656
14777
  this.checkpoint.resolvedBoundaries = {
14657
14778
  ...(this.checkpoint.resolvedBoundaries ?? {}),
14658
14779
  [boundaryId]: {
14780
+ ...existing,
14659
14781
  kind: 'sleep',
14660
14782
  delayMs,
14783
+ ...(wakeAtMs !== undefined ? { wakeAtMs } : {}),
14661
14784
  },
14662
14785
  };
14786
+ if (
14787
+ wakeAtMs !== undefined &&
14788
+ !rowStore &&
14789
+ this.durableDirectToolResultsBackedByReceipts &&
14790
+ (wakeAtMs <= Date.now() ||
14791
+ (delayMs <= AUTHORED_SLEEP_RESIDENT_LIMIT_MS &&
14792
+ wakeAtMs < (this.#options.executionDrainAtMs ?? Infinity)))
14793
+ ) {
14794
+ const remainingMs = Math.max(0, wakeAtMs - Date.now());
14795
+ if (remainingMs > 0)
14796
+ await this.sleepWithCheckpointHeartbeat(remainingMs);
14797
+ this.assertNoPendingDurableSuspension();
14798
+ this.checkpoint.resolvedBoundaries[boundaryId] = {
14799
+ ...this.checkpoint.resolvedBoundaries[boundaryId],
14800
+ kind: 'sleep',
14801
+ delayMs,
14802
+ wakeAtMs,
14803
+ completedAt: Date.now(),
14804
+ };
14805
+ this.#options.onBatchComplete?.(this.checkpoint);
14806
+ return;
14807
+ }
14663
14808
  this.suspendDurably({
14664
14809
  kind: 'sleep',
14665
14810
  boundaryId,
14666
14811
  delayMs,
14812
+ ...(wakeAtMs !== undefined ? { wakeAtMs } : {}),
14667
14813
  });
14668
14814
  }
14669
14815
  return new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -15000,30 +15146,33 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15000
15146
  const boundaryId = this.durableBoundaryId(
15001
15147
  `step-${scope}:${normalizedKey}${boundarySuffix}`,
15002
15148
  );
15149
+ const advanceRecoveredSleepCursor = async (
15150
+ receipt?: RuntimeStepReceipt,
15151
+ ) => {
15152
+ if (rowStore?.replayCallIndexes?.recoverStep(boundaryId)) return;
15153
+ const existing = assertKnownResolvedBoundary(
15154
+ boundaryId,
15155
+ this.checkpoint.resolvedBoundaries?.[boundaryId],
15156
+ );
15157
+ const sleepCount =
15158
+ existing?.kind === 'step' && existing.sleepCount !== undefined
15159
+ ? existing.sleepCount
15160
+ : receipt?.runId === this.currentReceiptOwnerRunId
15161
+ ? ((await this.readStepSleepCount(boundaryId)) ?? 0)
15162
+ : 0;
15163
+ if (Number.isSafeInteger(sleepCount) && sleepCount > 0)
15164
+ this.sleepBoundaryIndex += sleepCount;
15165
+ };
15003
15166
  const executeStep = async (): Promise<T> => {
15004
15167
  const existing = assertKnownResolvedBoundary(
15005
15168
  boundaryId,
15006
15169
  this.checkpoint.resolvedBoundaries?.[boundaryId],
15007
15170
  );
15008
15171
  if (existing?.kind === 'step' && 'output' in existing) {
15009
- const replayedCalls =
15010
- rowStore?.replayCallIndexes?.recoverStep(boundaryId) ?? false;
15011
15172
  this.runtimeLog(
15012
15173
  `ctx.step(${normalizedKey}): recovered result from checkpoint`,
15013
15174
  );
15014
- // The step callback is not evaluated on replay, so any sleeps that it
15015
- // contained would otherwise disappear from the positional sleep
15016
- // cursor. Preserve the authored boundary sequence before evaluating
15017
- // the next sibling operation. This is deliberately run-local state;
15018
- // it is not a cross-run sleep cache or an input-derived identity.
15019
- if (
15020
- !replayedCalls &&
15021
- Number.isSafeInteger(existing.sleepCount) &&
15022
- existing.sleepCount !== undefined &&
15023
- existing.sleepCount > 0
15024
- ) {
15025
- this.sleepBoundaryIndex += existing.sleepCount;
15026
- }
15175
+ await advanceRecoveredSleepCursor();
15027
15176
  return existing.output as T;
15028
15177
  }
15029
15178
 
@@ -15046,15 +15195,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15046
15195
  // publish an enclosing step fallback that would make a later resume skip
15047
15196
  // the suspended work.
15048
15197
  this.assertNoPendingDurableSuspension();
15198
+ const sleepCount = rowStore?.replayCallIndexes
15199
+ ? (callAdvances.get('sleep:legacy') ?? 0)
15200
+ : this.sleepBoundaryIndex - sleepBoundaryIndexBefore;
15201
+ if (sleepCount > 0 && this.durableDirectToolResultsBackedByReceipts) {
15202
+ await this.persistStepSleepCount(boundaryId, sleepCount);
15203
+ }
15204
+ this.assertNoPendingDurableSuspension();
15049
15205
  this.checkpoint.resolvedBoundaries = {
15050
15206
  ...(this.checkpoint.resolvedBoundaries ?? {}),
15051
15207
  [boundaryId]: {
15052
15208
  kind: 'step',
15053
15209
  stepId: normalizedKey,
15054
15210
  output,
15055
- sleepCount: rowStore?.replayCallIndexes
15056
- ? (callAdvances.get('sleep:legacy') ?? 0)
15057
- : this.sleepBoundaryIndex - sleepBoundaryIndexBefore,
15211
+ sleepCount,
15058
15212
  completedAt: Date.now(),
15059
15213
  },
15060
15214
  };
@@ -15099,8 +15253,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15099
15253
  : options?.semanticKey,
15100
15254
  staleAfterSeconds: options?.staleAfterSeconds,
15101
15255
  force: this.#options.cachePolicy?.forceStepRefresh === true,
15102
- markSkipped: (output) => {
15256
+ markSkipped: async (output, receipt) => {
15103
15257
  assertJsonSerializableStepOutput(normalizedKey, output);
15258
+ await advanceRecoveredSleepCursor(receipt);
15104
15259
  },
15105
15260
  execute: executeStep,
15106
15261
  },
@@ -17849,8 +18004,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17849
18004
  const executionRequestNonce = crypto.randomUUID();
17850
18005
  // Transport ambiguity must retain the billing identity. An ordinary
17851
18006
  // (unfenced) call advances it after a definite rejection released
17852
- // that attempt's reservation, including app pacing refusals. Fenced
17853
- // calls retain it: gateway-qualified retries own their distinct charge lifecycle.
18007
+ // that attempt's reservation. A proven app pacing refusal also
18008
+ // renews fenced billing attribution, keeping its invocation and
18009
+ // provider-operation identities unchanged. Ambiguous deliveries retain it.
17854
18010
  let deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
17855
18011
  // The provider operation has a semantic identity independent of
17856
18012
  // the response receipt. Its key is supplied by the caller that owns
@@ -18647,13 +18803,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18647
18803
  // settle the row as failed instead of pacing.
18648
18804
  [TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER]:
18649
18805
  TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
18650
- ...(!usesDurableInvocationFence &&
18651
- !providerExecutionFenceKey
18652
- ? {
18653
- [TOOL_EXECUTE_APP_PACING_REFUSAL_HEADER]:
18654
- TOOL_EXECUTE_APP_PACING_REFUSAL_VALUE,
18655
- }
18656
- : {}),
18806
+ [TOOL_EXECUTE_APP_PACING_REFUSAL_HEADER]:
18807
+ TOOL_EXECUTE_APP_PACING_REFUSAL_VALUE,
18657
18808
  ...(providerGatewayDispatchProof
18658
18809
  ? {
18659
18810
  [TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER]:
@@ -18698,18 +18849,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18698
18849
  recordProviderDispatch();
18699
18850
  if (dispatchedResponse.status === 429) {
18700
18851
  const observedAt = Date.now();
18701
- const retryAfter =
18702
- dispatchedResponse.headers.get('retry-after');
18703
18852
  // Preserve absolute provider deadlines even when the
18704
18853
  // response did not carry the gateway's counter
18705
18854
  // attestation. Classification below still decides
18706
18855
  // whether this 429 may update shared provider pacing.
18707
- providerRetryAtMs = retryAfter?.trim()
18708
- ? parseToolExecuteRetryAtMs(
18709
- retryAfter,
18710
- observedAt,
18711
- )
18712
- : undefined;
18856
+ providerRetryAtMs =
18857
+ parseToolExecuteProviderRetryAtMs(
18858
+ dispatchedResponse,
18859
+ observedAt,
18860
+ );
18713
18861
  }
18714
18862
  if (
18715
18863
  isAttestedProvider429Response(dispatchedResponse)
@@ -18877,6 +19025,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18877
19025
  schemaVersion: toolErrorSchemaVersion,
18878
19026
  retryAfterHeader:
18879
19027
  dispatchedResponse.headers.get('retry-after'),
19028
+ retryAtMs:
19029
+ parseToolExecuteExactProviderRetryAtMs(
19030
+ dispatchedResponse,
19031
+ ),
18880
19032
  transientHttpRetrySafe:
18881
19033
  retrySafeTransientHttp || fixtureReplaySafe,
18882
19034
  executionFenced: usesDurableInvocationFence,
@@ -18898,6 +19050,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18898
19050
  schemaVersion: toolErrorSchemaVersion,
18899
19051
  retryAfterHeader:
18900
19052
  dispatchedResponse.headers.get('retry-after'),
19053
+ retryAtMs:
19054
+ parseToolExecuteExactProviderRetryAtMs(
19055
+ dispatchedResponse,
19056
+ ),
18901
19057
  transientHttpRetrySafe:
18902
19058
  retrySafeTransientHttp || fixtureReplaySafe,
18903
19059
  executionFenced: usesDurableInvocationFence,
@@ -19294,6 +19450,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19294
19450
  bodyText: text,
19295
19451
  schemaVersion: toolErrorSchemaVersion,
19296
19452
  retryAfterHeader: response.headers.get('retry-after'),
19453
+ retryAtMs: parseToolExecuteExactProviderRetryAtMs(response),
19297
19454
  transientHttpRetrySafe:
19298
19455
  retrySafeTransientHttp || fixtureReplaySafe,
19299
19456
  executionFenced: usesDurableInvocationFence,
@@ -19519,11 +19676,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19519
19676
  );
19520
19677
  logProvider429RetryPhase('retry_sleep_completed');
19521
19678
  if (
19522
- !usesDurableInvocationFence &&
19523
- (failure.backpressureDelayMs !== null ||
19524
- failure.reason === 'provider_pacing_wait' ||
19525
- (failure.reason === 'rate_limit' &&
19526
- failure.knownNotDispatched))
19679
+ failure.reason === 'provider_pacing_wait' ||
19680
+ (!usesDurableInvocationFence &&
19681
+ (failure.backpressureDelayMs !== null ||
19682
+ (failure.reason === 'rate_limit' &&
19683
+ failure.knownNotDispatched)))
19527
19684
  ) {
19528
19685
  // These refusals release the prior reservation. App pacing
19529
19686
  // does not advance invocationAttempt, so use its own HTTP
@@ -815,6 +815,8 @@ export interface ContextOptions {
815
815
  rowAdmissionSignal?: AbortSignal;
816
816
  /** Local runner cancellation; never serialized into a Play artifact. */
817
817
  executionSignal?: AbortSignal;
818
+ /** Runner-local deadline for stopping new work, captured when its timer arms. */
819
+ executionDrainAtMs?: number;
818
820
  /**
819
821
  * Runtime-sheet transport selected by the runner. Daytona sandboxes use the
820
822
  * execution gateway and must never fall back to minting direct DB sessions.
@@ -1244,6 +1246,8 @@ export interface PlayCheckpoint {
1244
1246
  | {
1245
1247
  kind: 'sleep';
1246
1248
  delayMs: number;
1249
+ /** Original wake deadline. Historical boundaries retain relative waits. */
1250
+ wakeAtMs?: number;
1247
1251
  completedAt?: number;
1248
1252
  scope?: {
1249
1253
  type: 'workflow' | 'map_row';
@@ -777,7 +777,10 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
777
777
  runningReceiptWaitMaxAttempts?: number;
778
778
  runningReceiptWaitDelayMs?: number;
779
779
  reclaimRunning?: boolean;
780
- markSkipped?: (output: T) => Promise<void> | void;
780
+ markSkipped?: (
781
+ output: T,
782
+ receipt: RuntimeStepReceipt,
783
+ ) => Promise<void> | void;
781
784
  onRecovered?: (
782
785
  output: T,
783
786
  receipt: RuntimeStepReceipt,
@@ -812,6 +815,9 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
812
815
  ): Promise<T> => {
813
816
  input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
814
817
  if (receipt.output === undefined) {
818
+ if (input.operation === 'step' && input.markSkipped) {
819
+ await input.markSkipped(receipt.output as T, receipt);
820
+ }
815
821
  return receipt.output as T;
816
822
  }
817
823
  const output = runtimeReceiptOutput<T>(receipt);
@@ -819,7 +825,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
819
825
  ? input.onRecovered(output, receipt, 'cache')
820
826
  : output;
821
827
  if (input.markSkipped) {
822
- await input.markSkipped(recovered);
828
+ await input.markSkipped(recovered, receipt);
823
829
  }
824
830
  return recovered;
825
831
  };
@@ -62,6 +62,14 @@ export type DurableInvocationDiagnostic = {
62
62
  error?: unknown;
63
63
  };
64
64
 
65
+ /** Authenticated transport proof that this owner sent no provider request. */
66
+ export class DurableInvocationDispatchNotStartedError extends Error {
67
+ constructor(readonly reason: string) {
68
+ super(reason);
69
+ this.name = 'DurableInvocationDispatchNotStartedError';
70
+ }
71
+ }
72
+
65
73
  export class DurableInvocationConflictError extends Error {
66
74
  readonly code = 'DURABLE_INVOCATION_REQUEST_CONFLICT';
67
75
 
@@ -313,6 +321,14 @@ export async function executeWithDurableInvocationFence(input: {
313
321
  response = await input.execute();
314
322
  } catch (error) {
315
323
  diagnose('dispatch_failed', error);
324
+ if (error instanceof DurableInvocationDispatchNotStartedError) {
325
+ // An authenticated app refusal proves no provider request was sent.
326
+ // Release only this owner; ordinary transport failures stay unknown.
327
+ if (!(await input.store.abandon(begun))) {
328
+ throw new DurableInvocationInProgressError();
329
+ }
330
+ throw error;
331
+ }
316
332
  if (input.replaySafeAfterDispatch) {
317
333
  return await retryReplaySafeInvocation(begun);
318
334
  }
@@ -140,7 +140,6 @@ function sleepWithAbort(
140
140
  * hold more than a moment of a shared provider window.
141
141
  */
142
142
  const RATE_BLOCK_HORIZON_MS = 1_000;
143
- const UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS = 500;
144
143
  const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
145
144
  /**
146
145
  * Jitter fraction applied to a server-supplied `waitMs` so a synchronized wave
@@ -667,23 +666,22 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
667
666
  // a hot map is a round trip per permit. The store's own token floor, not
668
667
  // this number, is what bounds the grant.
669
668
  const learnedRps = this.effectiveRps.get(laneKey);
669
+ const currentRps = learnedRps ?? configuredRps;
670
670
  const recoveringUnknownProvider =
671
671
  learnedRps !== undefined &&
672
672
  learnedRps < DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS &&
673
673
  rules.some((rule) => rule.ruleId === 'default:provider-scope') &&
674
674
  rules.every((rule) => rule.ruleId.startsWith('default:'));
675
- // Below recovered capacity, the next clean observation can double the
676
- // pace after 500ms. Do not reserve a full second at the old slower pace:
677
- // that schedule debt would delay grants even after learning succeeds.
675
+ // Below recovered capacity, one dispatch interval (500/250/125ms at
676
+ // 2/4/8 RPS) can prove the next step. Reserve only that interval so cached
677
+ // old-pace tickets cannot postpone the next fresh-feedback refill.
678
678
  const horizonMs = Math.min(
679
- recoveringUnknownProvider
680
- ? UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS
681
- : RATE_BLOCK_HORIZON_MS,
679
+ recoveringUnknownProvider ? 1_000 / currentRps : RATE_BLOCK_HORIZON_MS,
682
680
  leasedBlockTtlMs(rules),
683
681
  );
684
682
  const rateBudget = Math.max(
685
683
  1,
686
- Math.floor(((learnedRps ?? configuredRps) * horizonMs) / 1_000),
684
+ Math.floor((currentRps * horizonMs) / 1_000),
687
685
  );
688
686
  if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
689
687
  const waiters = this.waiters.get(laneKey) ?? 1;
@@ -1018,7 +1016,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1018
1016
  const releaseGeneration = releaseSignal?.generation;
1019
1017
  const successKey = pendingSuccessKey(bucketId, ordered);
1020
1018
  const feedback = this.snapshotSuccessFeedback(successKey);
1021
- const response = await this.port.acquire({
1019
+ const requestStartedAtMs = this.now();
1020
+ const rawResponse = await this.port.acquire({
1022
1021
  bucketId,
1023
1022
  rateScopeToken,
1024
1023
  rules: withAdaptiveMaxRps(ordered),
@@ -1026,6 +1025,28 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1026
1025
  ...feedback,
1027
1026
  schedulerSchema: this.schedulerSchema,
1028
1027
  });
1028
+ const serverElapsedMs = rawResponse.serverClockElapsedMs;
1029
+ const validServerElapsed =
1030
+ typeof rawResponse.serverNowMs === 'number' &&
1031
+ Number.isFinite(rawResponse.serverNowMs) &&
1032
+ rawResponse.serverNowMs >= 0 &&
1033
+ typeof serverElapsedMs === 'number' &&
1034
+ Number.isSafeInteger(serverElapsedMs) &&
1035
+ serverElapsedMs >= 0 &&
1036
+ serverElapsedMs <= this.now() - requestStartedAtMs;
1037
+ // The clock was already this old when the response left the server.
1038
+ // Advance only by confirmed elapsed time; unknown transit remains
1039
+ // conservative, and an invalid/legacy response keeps the old clock.
1040
+ const response = validServerElapsed
1041
+ ? {
1042
+ ...rawResponse,
1043
+ serverNowMs: rawResponse.serverNowMs! + serverElapsedMs,
1044
+ waitMs:
1045
+ rawResponse.waitMs > 0
1046
+ ? Math.max(1, rawResponse.waitMs - serverElapsedMs)
1047
+ : rawResponse.waitMs,
1048
+ }
1049
+ : rawResponse;
1029
1050
  this.acknowledgeSuccessFeedback(successKey, feedback);
1030
1051
  this.recordCooldown(bucketId, response);
1031
1052
  if (response.scopeValidationSupported === true)
@@ -96,6 +96,12 @@ export type PlayRunnerRateStateAcquireResult = {
96
96
  * provider spacing.
97
97
  */
98
98
  serverNowMs?: number;
99
+ /**
100
+ * Confirmed elapsed time after receiving the database clock and before
101
+ * returning this grant. This is a lower bound on the clock's age: uncertain
102
+ * database and HTTP transit time is excluded. Absent on older receivers.
103
+ */
104
+ serverClockElapsedMs?: number;
99
105
  /**
100
106
  * One globally reserved dispatch timestamp per granted permit. The runtime
101
107
  * may cache the block, but must not dispatch a permit before its timestamp.
@@ -1159,6 +1159,7 @@ export interface ExecutePlayRunnerOptions {
1159
1159
  signal?: AbortSignal;
1160
1160
  /** Stops future row admission while already admitted rows can persist. */
1161
1161
  rowAdmissionSignal?: AbortSignal;
1162
+ executionDrainAtMs?: number;
1162
1163
  /** Runner-local terminal state made available to the deadline drain. */
1163
1164
  onRuntimeReady?: (runtime: {
1164
1165
  log: (line: string) => void;
@@ -2036,14 +2037,15 @@ async function run(
2036
2037
  runnerRunAttempt,
2037
2038
  );
2038
2039
  const workCounterSegmentId = randomUUID();
2040
+ const runtimeLimitMs =
2041
+ validatePlaySandboxRuntimeLimits(
2042
+ config.context.sandboxRuntimeLimits ??
2043
+ STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
2044
+ ).timeoutSeconds * 1_000;
2039
2045
  const persistenceSignal =
2040
- options?.signal ??
2041
- AbortSignal.timeout(
2042
- validatePlaySandboxRuntimeLimits(
2043
- config.context.sandboxRuntimeLimits ??
2044
- STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
2045
- ).timeoutSeconds * 1_000,
2046
- );
2046
+ options?.signal ?? AbortSignal.timeout(runtimeLimitMs);
2047
+ const executionDrainAtMs =
2048
+ options?.executionDrainAtMs ?? runStartedAt + runtimeLimitMs;
2047
2049
  const contextSetupStartedAt = Date.now();
2048
2050
  const getRuntimeToolMetadata = createRuntimeToolMetadataResolver(config);
2049
2051
  const getRuntimeToolAuthScopeDigest =
@@ -2225,6 +2227,7 @@ async function run(
2225
2227
  artifactRuntimeContract,
2226
2228
  rowAdmissionSignal: options?.rowAdmissionSignal,
2227
2229
  executionSignal: persistenceSignal,
2230
+ executionDrainAtMs,
2228
2231
  executorToken: config.context.executorToken,
2229
2232
  baseUrl: config.context.baseUrl,
2230
2233
  // Runtime control stays on baseUrl (the receipt gateway); provider calls
@@ -3894,6 +3897,7 @@ async function main() {
3894
3897
  result = await executePlayRunner(config, {
3895
3898
  signal: executionController.signal,
3896
3899
  rowAdmissionSignal: rowAdmissionController.signal,
3900
+ executionDrainAtMs: runtimeDeadline.drainAtMs,
3897
3901
  onRuntimeReady: (runtime) => {
3898
3902
  runtimeState = runtime;
3899
3903
  },