deepline 0.3.452 → 0.3.454

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/client.ts +65 -0
  2. package/dist/bundling-sources/sdk/src/http.ts +2 -1
  3. package/dist/bundling-sources/sdk/src/index.ts +2 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  6. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +63 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +13 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +83 -4
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +11 -3
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +16 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +20 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +77 -12
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +5 -2
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +254 -64
  16. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +11 -0
  17. package/dist/cli/auth-GUZWKNXM.js +40 -0
  18. package/dist/cli/{auth-DKZLB6GN.mjs → auth-YNPT3IER.mjs} +4 -4
  19. package/dist/cli/{billing-DOFXWAZR.mjs → billing-TLRVAOXB.mjs} +18 -5
  20. package/dist/cli/{billing-CN3XSVER.js → billing-VXIIND6T.js} +87 -74
  21. package/dist/cli/{chunk-TXN27XQQ.mjs → chunk-27NNEBIS.mjs} +2 -2
  22. package/dist/cli/chunk-2I7GE7KB.mjs +155 -0
  23. package/dist/cli/{chunk-M4FFNUF4.js → chunk-33FEF4RH.js} +50 -50
  24. package/dist/cli/{chunk-FIQ2F5LA.mjs → chunk-5GJH2BYN.mjs} +2 -2
  25. package/dist/cli/{chunk-ELULDUX3.mjs → chunk-5OQMUDPM.mjs} +1 -1
  26. package/dist/cli/{chunk-5DRGDSJL.js → chunk-6JY2NGJN.js} +26 -26
  27. package/dist/cli/chunk-7ICOQRW5.js +155 -0
  28. package/dist/cli/{chunk-7JSZOUNU.js → chunk-7V3OKSB2.js} +1 -1
  29. package/dist/cli/{chunk-74BPYDDG.js → chunk-BUYQU7TB.js} +8 -8
  30. package/dist/cli/{chunk-RNM4HG3W.mjs → chunk-CPXT4KPC.mjs} +3 -3
  31. package/dist/cli/{chunk-KQUDU3ZE.mjs → chunk-DCZSJON4.mjs} +2 -2
  32. package/dist/cli/{chunk-2GMAPSJU.mjs → chunk-EXWKHMMP.mjs} +5 -5
  33. package/dist/cli/{chunk-ZH6HFV7Y.js → chunk-FKGXZXRP.js} +8 -8
  34. package/dist/cli/{chunk-UGPGB4C6.mjs → chunk-G6ZOZQXT.mjs} +4 -153
  35. package/dist/cli/{chunk-L2GATUZK.js → chunk-IJ5WCZ32.js} +20 -20
  36. package/dist/cli/{chunk-JT4ZSWBK.js → chunk-KPPL2WNH.js} +16 -16
  37. package/dist/cli/{chunk-X4IMX5AO.mjs → chunk-LVHRP7XM.mjs} +4 -4
  38. package/dist/cli/{chunk-RFDIXLRX.mjs → chunk-LWYOJG66.mjs} +1 -1
  39. package/dist/cli/{chunk-L6YCGSL4.js → chunk-P5IQ6LIK.js} +35 -35
  40. package/dist/cli/{chunk-2VERHTKO.js → chunk-QLMQ2NAQ.js} +2 -2
  41. package/dist/cli/{chunk-PBQSX6ES.mjs → chunk-RDAUQGPG.mjs} +1 -1
  42. package/dist/cli/{chunk-7OI33KE5.js → chunk-S2PO3X5R.js} +2 -2
  43. package/dist/cli/{chunk-ZKGK2JU3.js → chunk-SJH66N73.js} +6 -6
  44. package/dist/cli/{chunk-SM5RZ5JC.mjs → chunk-TECW24CR.mjs} +2 -2
  45. package/dist/cli/{chunk-R7YZKHN5.mjs → chunk-THJ7ORNM.mjs} +3 -3
  46. package/dist/cli/{chunk-POSAVPQZ.mjs → chunk-U72KDMYJ.mjs} +2 -2
  47. package/dist/cli/{chunk-JJOSUZMS.mjs → chunk-UT6W675P.mjs} +1 -1
  48. package/dist/cli/{chunk-G5BMPTZD.js → chunk-VHGGHRGY.js} +81 -81
  49. package/dist/cli/{chunk-ETFVH7PO.js → chunk-VXBEHDM6.js} +10 -10
  50. package/dist/cli/{chunk-WW63S4RC.js → chunk-WONLZMDF.js} +6 -6
  51. package/dist/cli/{chunk-QK65KBWN.mjs → chunk-YETNOEAZ.mjs} +8 -8
  52. package/dist/cli/{chunk-YWL4ULLT.js → chunk-YPYR4ACF.js} +73 -222
  53. package/dist/cli/{chunk-2SICEJBN.mjs → chunk-YXCLG3KB.mjs} +35 -1
  54. package/dist/cli/{chunk-J5O4AXSO.js → chunk-Z5PQSTYX.js} +84 -50
  55. package/dist/cli/{csv-N25FMDZQ.js → csv-BSKJR3CR.js} +12 -12
  56. package/dist/cli/{csv-QUNUQZYC.mjs → csv-I4CAAV6A.mjs} +3 -3
  57. package/dist/cli/{db-WYWDUYXS.mjs → db-EQP24K3B.mjs} +4 -4
  58. package/dist/cli/{db-2SGG56WR.js → db-RA3IL6MC.js} +25 -25
  59. package/dist/cli/{describe-QQ2XL6PV.js → describe-62KPTIFH.js} +17 -16
  60. package/dist/cli/{describe-4RBUIHR7.mjs → describe-EZDVJDPQ.mjs} +13 -12
  61. package/dist/cli/describe-core.d.mts +1 -1
  62. package/dist/cli/describe-core.d.ts +1 -1
  63. package/dist/cli/{executions-YLJHAHHX.js → executions-2HBBXRPM.js} +6 -6
  64. package/dist/cli/{executions-WOBIMQ7H.mjs → executions-W5KR56KP.mjs} +4 -4
  65. package/dist/cli/{feedback-VCGVROTY.mjs → feedback-6UTOLSFP.mjs} +3 -3
  66. package/dist/cli/{feedback-D653FAXY.js → feedback-CVYS7ZPV.js} +7 -7
  67. package/dist/cli/{git-SZ6RSLFN.mjs → git-JXH64LI4.mjs} +4 -4
  68. package/dist/cli/git-U6FMVAAM.js +24 -0
  69. package/dist/cli/index.js +133 -131
  70. package/dist/cli/index.mjs +39 -37
  71. package/dist/cli/{legacy-noops-TXZ75CWS.js → legacy-noops-5XTXIPU2.js} +7 -7
  72. package/dist/cli/{legacy-noops-UGLTI2ZY.mjs → legacy-noops-NYMLGQNX.mjs} +4 -4
  73. package/dist/cli/{monitors-HLIW4AIL.mjs → monitors-CEU6SP2B.mjs} +156 -4
  74. package/dist/cli/{monitors-VC6YSBYX.js → monitors-J6HAQLX2.js} +276 -124
  75. package/dist/cli/{org-I3VQDIGZ.js → org-CYE3DCIT.js} +66 -66
  76. package/dist/cli/{org-X4MVTSKM.mjs → org-RRKAJM5V.mjs} +4 -4
  77. package/dist/cli/{play-7TLGGN66.js → play-NJDW74YH.js} +284 -283
  78. package/dist/cli/{play-EAYGEAMI.mjs → play-PXISXZ2P.mjs} +11 -10
  79. package/dist/cli/{providers-MBTMUEGX.mjs → providers-6QGGDOWK.mjs} +4 -4
  80. package/dist/cli/{providers-IZ3YWZ4E.js → providers-JE3MUBYK.js} +7 -7
  81. package/dist/cli/{quickstart-MBDEV4RJ.js → quickstart-AQBBOFQL.js} +5 -5
  82. package/dist/cli/{quickstart-MDW2NMAS.mjs → quickstart-MKJ7TYQZ.mjs} +3 -3
  83. package/dist/cli/{search-4PQZYRQO.mjs → search-6PKFZ2JD.mjs} +5 -5
  84. package/dist/cli/search-EJGRNQQK.js +30 -0
  85. package/dist/cli/{secrets-3S7KUS2H.js → secrets-IKGJYESD.js} +13 -13
  86. package/dist/cli/{secrets-S55UCYYR.mjs → secrets-QBWRVZCT.mjs} +4 -4
  87. package/dist/cli/{sessions-UEHVIB6W.js → sessions-462I5YEQ.js} +5 -5
  88. package/dist/cli/{sessions-T6XVN737.mjs → sessions-SVYTBNRJ.mjs} +4 -4
  89. package/dist/cli/{settings-CGP7XMJZ.mjs → settings-FH4XZ7FE.mjs} +9 -8
  90. package/dist/cli/{settings-MYAUWNTF.js → settings-WHLTLPXZ.js} +76 -75
  91. package/dist/cli/{setup-5Z5HAWEG.js → setup-IKTCP3QB.js} +43 -42
  92. package/dist/cli/{setup-U66NZQXB.mjs → setup-ISCNCB4W.mjs} +8 -7
  93. package/dist/cli/{skills-2JS3SNFA.mjs → skills-HITZS5EJ.mjs} +6 -5
  94. package/dist/cli/skills-LC2YKUSO.js +30 -0
  95. package/dist/cli/{tools-G3VYZKTO.mjs → tools-SQUTVLPO.mjs} +13 -12
  96. package/dist/cli/tools-Y55OXE72.js +45 -0
  97. package/dist/cli/{update-C7CLWWIW.mjs → update-AFYEREBX.mjs} +8 -7
  98. package/dist/cli/update-Q3I6MSEQ.js +32 -0
  99. package/dist/cli/{workflow-TBKVIN4S.js → workflow-AAOOROIR.js} +15 -15
  100. package/dist/cli/{workflow-EUMRVXU7.mjs → workflow-VBCZPKVF.mjs} +5 -5
  101. package/dist/{client-CPRaigLC.d.ts → client-BV7tC6oq.d.ts} +28 -1
  102. package/dist/{client-DstDI5TV.d.mts → client-Ct6OBj9Y.d.mts} +28 -1
  103. package/dist/index.d.mts +2 -2
  104. package/dist/index.d.ts +2 -2
  105. package/dist/index.js +6 -6
  106. package/dist/index.mjs +3 -3
  107. package/dist/install-integrity.json +82 -80
  108. package/dist/plays/play-description.d.mts +1 -1
  109. package/dist/plays/play-description.d.ts +1 -1
  110. package/dist/plays/run-request.d.mts +1 -1
  111. package/dist/plays/run-request.d.ts +1 -1
  112. package/dist/release.d.mts +1 -1
  113. package/dist/release.d.ts +1 -1
  114. package/dist/release.js +2 -2
  115. package/dist/release.mjs +1 -1
  116. package/package.json +1 -1
  117. package/dist/cli/auth-36DQP46D.js +0 -40
  118. package/dist/cli/git-T6LBJEY6.js +0 -24
  119. package/dist/cli/search-5B4ZM7VS.js +0 -30
  120. package/dist/cli/skills-JXPQFJAJ.js +0 -29
  121. package/dist/cli/tools-RHRJ4SLY.js +0 -44
  122. package/dist/cli/update-5YBPJ3EQ.js +0 -31
@@ -1285,6 +1285,18 @@ export type MonitorsListOptions = {
1285
1285
  * plans, dependents) and it must not require an SDK bump to evolve.
1286
1286
  */
1287
1287
  export type MonitorCheckResult = Record<string, unknown>;
1288
+
1289
+ export type MonitorPreviewOptions = {
1290
+ /** Sample size, 1-3. The server default is 3. */
1291
+ limit?: number;
1292
+ /** Reuse to recover one preview's result; a new key is a new, paid preview. */
1293
+ idempotencyKey?: string;
1294
+ };
1295
+
1296
+ /** A preview's sample and charge, plus the key that identifies it. */
1297
+ export type MonitorPreviewResult = Record<string, unknown> & {
1298
+ idempotency_key: string;
1299
+ };
1288
1300
  export type MonitorDeployResult = Record<string, unknown>;
1289
1301
  /** Field-level monitor capability spec returned by `client.monitors.get(...)`. */
1290
1302
  export type MonitorSpec = {
@@ -1868,6 +1880,15 @@ export type MonitorsNamespace = {
1868
1880
  ) => Promise<MonitorsAvailableResult>;
1869
1881
  /** Validate a monitor definition without deploying it (no spend). */
1870
1882
  check: (definition: MonitorDefinition) => Promise<MonitorCheckResult>;
1883
+ /**
1884
+ * Sample a few recent matches for a definition before deploying it. Spends
1885
+ * Deepline credits per returned update. Reuse `idempotencyKey` to recover a
1886
+ * lost result without paying again; omit it for a new preview.
1887
+ */
1888
+ preview: (
1889
+ definition: MonitorDefinition,
1890
+ options?: MonitorPreviewOptions,
1891
+ ) => Promise<MonitorPreviewResult>;
1871
1892
  /** Deploy a monitor from a definition. May spend Deepline credits. */
1872
1893
  deploy: (
1873
1894
  definition: MonitorDefinition,
@@ -2850,6 +2871,8 @@ export class DeeplineClient {
2850
2871
  available: (toolIdOrOptions, options) =>
2851
2872
  this.getMonitorsAvailable(toolIdOrOptions, options),
2852
2873
  check: (definition) => this.checkMonitor(definition),
2874
+ preview: (definition, options) =>
2875
+ this.previewMonitor(definition, options),
2853
2876
  deploy: (definition, options) => this.deployMonitor(definition, options),
2854
2877
  sync: (definition, options) =>
2855
2878
  options?.dryRun
@@ -6686,6 +6709,48 @@ export class DeeplineClient {
6686
6709
  });
6687
6710
  }
6688
6711
 
6712
+ /**
6713
+ * Run one paid preview. Prefer `client.monitors.preview(...)`.
6714
+ *
6715
+ * The key is fixed before the request is sent and returned on the result
6716
+ * and on any error (`error.idempotencyKey`), so a caller that loses the
6717
+ * response can repeat the call with it and get the original result without
6718
+ * a second charge. The submission is never replayed automatically.
6719
+ */
6720
+ async previewMonitor(
6721
+ definition: MonitorDefinition,
6722
+ options?: MonitorPreviewOptions,
6723
+ ): Promise<MonitorPreviewResult> {
6724
+ const idempotencyKey =
6725
+ options?.idempotencyKey ??
6726
+ `monitor-preview-${
6727
+ globalThis.crypto?.randomUUID?.() ??
6728
+ `${Date.now()}-${Math.random().toString(36).slice(2)}`
6729
+ }`;
6730
+ try {
6731
+ const result = await this.http.request<Record<string, unknown>>(
6732
+ '/api/v2/monitors/preview',
6733
+ {
6734
+ method: 'POST',
6735
+ body: {
6736
+ ...definition,
6737
+ ...(options?.limit === undefined ? {} : { limit: options.limit }),
6738
+ },
6739
+ headers: { 'Idempotency-Key': idempotencyKey },
6740
+ maxRetries: 0,
6741
+ exactUrlOnly: true,
6742
+ preserveRateLimitResponse: true,
6743
+ },
6744
+ );
6745
+ return { ...result, idempotency_key: idempotencyKey };
6746
+ } catch (error) {
6747
+ if (error && typeof error === 'object') {
6748
+ Object.assign(error, { idempotencyKey });
6749
+ }
6750
+ throw error;
6751
+ }
6752
+ }
6753
+
6689
6754
  /**
6690
6755
  * Deploy a monitor from a definition. `dryRun` validates via the check
6691
6756
  * endpoint and returns the plan without deploying. Prefer
@@ -1036,7 +1036,8 @@ function isMonitorRateLimitEnvelope(parsed: unknown): boolean {
1036
1036
  const response = asRecord(parsed);
1037
1037
  const error = asRecord(response?.error);
1038
1038
  return (
1039
- error?.code === 'monitor_upstream_rate_limited' &&
1039
+ (error?.code === 'monitor_upstream_rate_limited' ||
1040
+ error?.code === 'monitor_preview_rate_limited') &&
1040
1041
  typeof error.message === 'string'
1041
1042
  );
1042
1043
  }
@@ -140,6 +140,8 @@ export type {
140
140
  LegacyMonitorStatusFilter,
141
141
  MonitorListEntry,
142
142
  MonitorCheckResult,
143
+ MonitorPreviewOptions,
144
+ MonitorPreviewResult,
143
145
  MonitorDeployResult,
144
146
  MonitorDetail,
145
147
  MonitorSpec,
@@ -205,7 +205,7 @@ export const SDK_RELEASE = {
205
205
  // getters keep their established compatibility behavior.
206
206
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
207
207
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
208
- version: '0.3.452',
208
+ version: '0.3.454',
209
209
  updateSummary: {
210
210
  introducedIn: '0.3.90',
211
211
  summary:
@@ -882,6 +882,8 @@ export interface PlayRunPackage {
882
882
  startedAt?: number | null;
883
883
  finishedAt?: number | null;
884
884
  durationMs?: number | null;
885
+ /** The provider that ran the sandbox, known once the run has ended. */
886
+ sandboxProvider?: 'daytona' | 'modal';
885
887
  outcome?: PlayRunOutcome;
886
888
  recovery?: {
887
889
  mode: 'replayed' | 'forced' | 'recovered' | 'joined';
@@ -286,6 +286,15 @@ export type ProviderMonitorCapabilityDefinition = {
286
286
  maxIds: number;
287
287
  rateLimitId: string;
288
288
  };
289
+ /**
290
+ * Optional paid sample: a few recent matches for the definition being
291
+ * considered, with nothing created. Only payloads whose `payloadField`
292
+ * value is listed can be previewed; every other payload of this capability,
293
+ * and every capability without this field, has no preview. `action` must
294
+ * also appear in `supportActions`, which is what registers it for
295
+ * execution.
296
+ */
297
+ preview?: ProviderMonitorPreviewDescriptor;
289
298
  // Declares that this monitor delivers data to Deepline via an inbound
290
299
  // webhook callback (handled by the provider-callbacks route). Webhook
291
300
  // subscription monitors are inherently inbound; native monitors must set
@@ -349,6 +358,43 @@ const UNLOCKED_ENTITLEMENT_STATUS: ProviderMonitorCapabilityEntitlementStatus =
349
358
  locked: false,
350
359
  };
351
360
 
361
+ export type ProviderMonitorPreviewDescriptor = {
362
+ action: string;
363
+ payloadField: string;
364
+ supportedValues: readonly string[];
365
+ defaultLimit: number;
366
+ maxLimit: number;
367
+ };
368
+
369
+ export type ProviderMonitorPreviewResolution =
370
+ | { available: true; descriptor: ProviderMonitorPreviewDescriptor }
371
+ | { available: false; reason: string };
372
+
373
+ /**
374
+ * Whether this payload of this capability can be previewed. `null` means the
375
+ * capability declares no preview at all, so callers omit preview entirely
376
+ * rather than saying "unavailable" for every monitor.
377
+ */
378
+ export function resolveProviderMonitorPreview(input: {
379
+ capability: Pick<
380
+ ProviderMonitorCapabilityDefinition,
381
+ 'providerCapability' | 'runtime'
382
+ >;
383
+ payload: Record<string, unknown>;
384
+ }): ProviderMonitorPreviewResolution | null {
385
+ const descriptor = input.capability.runtime.preview;
386
+ if (!descriptor) return null;
387
+ const value = input.payload[descriptor.payloadField];
388
+ if (typeof value === 'string' && descriptor.supportedValues.includes(value)) {
389
+ return { available: true, descriptor };
390
+ }
391
+ const label = typeof value === 'string' && value ? value : 'this payload';
392
+ return {
393
+ available: false,
394
+ reason: `Preview isn't available for ${label} yet. Run monitors check, then deploy.`,
395
+ };
396
+ }
397
+
352
398
  export type ProviderMonitorCapabilitySerializable = {
353
399
  provider: string;
354
400
  tool: string;
@@ -381,6 +427,11 @@ export type ProviderMonitorCapabilitySerializable = {
381
427
  included_active_monitors?: number | null;
382
428
  reason?: string;
383
429
  };
430
+ /** Present only when the capability declares a paid preview. */
431
+ preview?: {
432
+ available_for: Record<string, string[]>;
433
+ max_limit: number;
434
+ };
384
435
  next_commands: string[];
385
436
  };
386
437
 
@@ -604,6 +655,18 @@ export function serializeProviderMonitorCapability(
604
655
  : {}),
605
656
  ...(entitlement.reason ? { reason: entitlement.reason } : {}),
606
657
  },
658
+ ...(capability.runtime.preview
659
+ ? {
660
+ preview: {
661
+ available_for: {
662
+ [capability.runtime.preview.payloadField]: [
663
+ ...capability.runtime.preview.supportedValues,
664
+ ],
665
+ },
666
+ max_limit: capability.runtime.preview.maxLimit,
667
+ },
668
+ }
669
+ : {}),
607
670
  next_commands: [],
608
671
  };
609
672
  }
@@ -309,6 +309,9 @@ export interface RunnerPushExecutionConfig {
309
309
  leaseSeconds: number;
310
310
  /** The run the runner heartbeats. Must match the executor token's run scope. */
311
311
  runId: string;
312
+ /** The provider that acquired this sandbox, which a Daytona-selected launch
313
+ * can differ from after fallback. Absent from older workers. */
314
+ sandboxProvider?: 'daytona' | 'modal';
312
315
  }
313
316
 
314
317
  export interface PlayRunnerExecutionConfig {
@@ -4076,6 +4076,12 @@ async function main() {
4076
4076
  (
4077
4077
  result as PlayRunnerResult & { workCompletedAtMs?: number }
4078
4078
  ).workCompletedAtMs = workCompletedAtMs;
4079
+ if (runnerTerminalPushContext?.sandboxProvider) {
4080
+ result.stats = {
4081
+ ...result.stats,
4082
+ sandboxProvider: runnerTerminalPushContext.sandboxProvider,
4083
+ };
4084
+ }
4079
4085
  const terminalResult = runnerTerminalPushContext
4080
4086
  ? runnerTerminalResultForTransport(result)
4081
4087
  : null;
@@ -4260,6 +4266,7 @@ let runnerTerminalPushContext: {
4260
4266
  gatewayBaseUrl: string;
4261
4267
  executorToken: string;
4262
4268
  runId: string;
4269
+ sandboxProvider?: 'daytona' | 'modal';
4263
4270
  } | null = null;
4264
4271
 
4265
4272
  function terminalPushContextFromConfig(
@@ -4271,7 +4278,12 @@ function terminalPushContextFromConfig(
4271
4278
  if (!push || !gatewayBaseUrl || !executorToken) {
4272
4279
  return null;
4273
4280
  }
4274
- return { gatewayBaseUrl, executorToken, runId: push.runId };
4281
+ return {
4282
+ gatewayBaseUrl,
4283
+ executorToken,
4284
+ runId: push.runId,
4285
+ ...(push.sandboxProvider ? { sandboxProvider: push.sandboxProvider } : {}),
4286
+ };
4275
4287
  }
4276
4288
 
4277
4289
  async function pushRunnerTerminalIfConfigured(
@@ -16,7 +16,9 @@ import {
16
16
  } from '@daytonaio/sdk';
17
17
  import axios, { type InternalAxiosRequestConfig } from 'axios';
18
18
  import { Readable } from 'node:stream';
19
+ import { setTimeout as delay } from 'node:timers/promises';
19
20
  import type { DaytonaClientOptions } from '../../daytona-runtime-config';
21
+ import type { DaytonaDeleteRetryAuthorization } from './daytona-lifecycle';
20
22
  import {
21
23
  DAYTONA_CONTROL_TIMEOUT_MS,
22
24
  DaytonaControlTimeoutError,
@@ -83,6 +85,10 @@ const DAYTONA_SDK_ERROR_NAMES: ReadonlySet<string> = new Set([
83
85
  ]);
84
86
  const DAYTONA_PROVIDER_ERROR_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{1,63}$/;
85
87
  const DAYTONA_PROVIDER_DETAIL_PATTERN = / \[provider [^\]]*\]/g;
88
+ // A pass costs two provider calls and one fenced scheduler write, so a sandbox
89
+ // stuck in transition must not repeat it every 100 ms for the whole phase.
90
+ const DAYTONA_RETIREMENT_BACKOFF_INITIAL_MS = 100;
91
+ const DAYTONA_RETIREMENT_BACKOFF_MAX_MS = 5_000;
86
92
 
87
93
  /** A provider state from Daytona's published enum, or `unrecognized`. */
88
94
  export function daytonaSandboxStateForLog(state: unknown): string | null {
@@ -599,10 +605,15 @@ export function createDaytonaLaunchClient(
599
605
  phase.dispose();
600
606
  }
601
607
  },
602
- async delete(sandbox: Pick<Sandbox, 'id'>, timeoutSeconds = 30) {
608
+ async delete(
609
+ sandbox: Pick<Sandbox, 'id'>,
610
+ timeoutSeconds = 30,
611
+ retry?: DaytonaDeleteRetryAuthorization,
612
+ ) {
603
613
  // Cleanup must survive execution cancellation and must not trigger the
604
614
  // SDK's unawaited refreshDataSafe after a confirmed delete response.
605
615
  const phase = deadline(timeoutSeconds * 1_000);
616
+ let absenceOnlyConflict: DaytonaConflictError | null = null;
606
617
  try {
607
618
  const api = new SandboxApi(
608
619
  new Configuration({
@@ -610,10 +621,78 @@ export function createDaytonaLaunchClient(
610
621
  baseOptions: { ...configuration.baseOptions, signal: phase.signal },
611
622
  }),
612
623
  );
613
- await api.deleteSandbox(sandbox.id, undefined, {
614
- timeout: timeoutSeconds * 1_000,
615
- });
624
+ let backoffMs = DAYTONA_RETIREMENT_BACKOFF_INITIAL_MS;
625
+ for (;;) {
626
+ phase.signal.throwIfAborted();
627
+ if (!absenceOnlyConflict) {
628
+ try {
629
+ await api.deleteSandbox(sandbox.id, undefined, {
630
+ timeout: timeoutSeconds * 1_000,
631
+ });
632
+ return;
633
+ } catch (caught) {
634
+ const error = providerError(caught);
635
+ // A prior accepted delete can already have removed this exact ID.
636
+ if (retry && error instanceof DaytonaNotFoundError) return;
637
+ if (
638
+ !retry ||
639
+ !(error instanceof DaytonaConflictError) ||
640
+ error.statusCode !== 409
641
+ )
642
+ throw error;
643
+ if (
644
+ error.message.trim() ===
645
+ 'Sandbox was modified by another operation'
646
+ ) {
647
+ // This conflict does not authorize mutation replay. A concurrent
648
+ // cleanup is deleting this exact ID, so only prove its absence.
649
+ absenceOnlyConflict = error;
650
+ } else if (
651
+ !/^Sandbox state change in progress[.!]?$/i.test(
652
+ error.message.trim(),
653
+ )
654
+ ) {
655
+ throw error;
656
+ }
657
+ }
658
+ }
659
+ // Give the in-progress transition (including durable cleanup) its
660
+ // backoff before observing absence, so a closed resource need not
661
+ // be re-reported just to learn that deletion succeeded.
662
+ await delay(backoffMs, undefined, { signal: phase.signal });
663
+ backoffMs = Math.min(backoffMs * 2, DAYTONA_RETIREMENT_BACKOFF_MAX_MS);
664
+ // Reconcile only the acquired ID. Never resolve a name, start it, or
665
+ // infer absence from a failed/ambiguous provider read.
666
+ try {
667
+ await api.getSandbox(sandbox.id, undefined, undefined, {
668
+ signal: phase.signal,
669
+ });
670
+ } catch (caught) {
671
+ const error = providerError(caught);
672
+ if (error instanceof DaytonaNotFoundError) return;
673
+ throw error;
674
+ }
675
+ if (absenceOnlyConflict) continue;
676
+ // Authorization and every provider call share this ONE phase budget.
677
+ // A delayed ACK after expiry must never authorize another DELETE.
678
+ const signal = phase.signal;
679
+ signal.throwIfAborted();
680
+ await new Promise<void>((resolve, reject) => {
681
+ const abort = () => reject(signal.reason);
682
+ signal.addEventListener('abort', abort, { once: true });
683
+ Promise.resolve()
684
+ // Only managed callers get here: the DELETE branch rethrows
685
+ // every failure when retry is absent.
686
+ .then(retry!.authorizeRetry)
687
+ .then(resolve, reject)
688
+ .finally(() => signal.removeEventListener('abort', abort));
689
+ });
690
+ phase.signal.throwIfAborted();
691
+ }
616
692
  } catch (error) {
693
+ // Absence never arrived, so the provider's conflict is the real cause.
694
+ if (absenceOnlyConflict && phase.signal.aborted)
695
+ throw absenceOnlyConflict;
617
696
  throw providerError(error);
618
697
  } finally {
619
698
  phase.dispose();
@@ -59,13 +59,21 @@ const DAYTONA_NETWORK_ALLOW_LIST_ENV = 'DEEPLINE_DAYTONA_NETWORK_ALLOW_LIST';
59
59
 
60
60
  export const DAYTONA_CANCELLED_ERROR = 'Daytona play runner cancelled';
61
61
 
62
+ export type DaytonaDeleteRetryAuthorization = {
63
+ authorizeRetry: () => Promise<void>;
64
+ };
65
+
62
66
  export type DaytonaClient = {
63
67
  create(
64
68
  params: CreateSandboxFromSnapshotParams,
65
69
  options: { timeout: number },
66
70
  ): Promise<Sandbox>;
67
71
  get?(name: string): Promise<Sandbox>;
68
- delete(sandbox: Pick<Sandbox, 'id'>, timeoutSeconds?: number): Promise<void>;
72
+ delete(
73
+ sandbox: Pick<Sandbox, 'id'>,
74
+ timeoutSeconds?: number,
75
+ retry?: DaytonaDeleteRetryAuthorization,
76
+ ): Promise<void>;
69
77
  scope(
70
78
  sandbox: Sandbox,
71
79
  timeoutMs: number,
@@ -84,7 +92,7 @@ type DaytonaCreateCallObserver = (
84
92
 
85
93
  export type AcquiredDaytonaSandbox = {
86
94
  sandbox: DaytonaSandbox;
87
- deleteSandbox: () => Promise<void>;
95
+ deleteSandbox: (retry?: DaytonaDeleteRetryAuthorization) => Promise<void>;
88
96
  daytonaOrganizationId: string;
89
97
  billingStartedAt: number;
90
98
  billingEndedAt?: number;
@@ -1117,7 +1125,7 @@ async function acquireOneShotDaytonaSandbox(input: {
1117
1125
  });
1118
1126
  return {
1119
1127
  sandbox,
1120
- deleteSandbox: () => input.daytona.delete(sandbox, 30),
1128
+ deleteSandbox: (retry) => input.daytona.delete(sandbox, 30, retry),
1121
1129
  daytonaOrganizationId,
1122
1130
  billingStartedAt,
1123
1131
  ...(result.sandboxCapacityLeaseId
@@ -560,6 +560,22 @@ async function timedRunnerPayloadUpload(input: {
560
560
  });
561
561
  }
562
562
 
563
+ /** Records the acquired provider in the config the sandbox reads. */
564
+ export function withRunnerSandboxProvider(
565
+ config: PlayRunnerExecutionConfig,
566
+ sandboxProvider: 'daytona' | 'modal',
567
+ ): PlayRunnerExecutionConfig {
568
+ const push = config.context.runnerPushExecution;
569
+ if (!push) return config;
570
+ return {
571
+ ...config,
572
+ context: {
573
+ ...config.context,
574
+ runnerPushExecution: { ...push, sandboxProvider },
575
+ },
576
+ };
577
+ }
578
+
563
579
  export async function stageRunnerPayload(input: {
564
580
  sandbox: RemoteRunnerPayloadSandbox;
565
581
  bundlePromise: Promise<string>;
@@ -189,6 +189,8 @@ let forceKillTimer = null;
189
189
  let runtimeTimer = null;
190
190
  let terminalGraceTimer = null;
191
191
  let heartbeatTimer = null;
192
+ let heartbeatRetryTimer = null;
193
+ let heartbeatCadenceMs = 0;
192
194
  let heartbeatController = null;
193
195
  let heartbeatInFlight = false;
194
196
  let heartbeatStopped = false;
@@ -236,6 +238,10 @@ function stopRunnerHeartbeat() {
236
238
  clearInterval(heartbeatTimer);
237
239
  heartbeatTimer = null;
238
240
  }
241
+ if (heartbeatRetryTimer) {
242
+ clearTimeout(heartbeatRetryTimer);
243
+ heartbeatRetryTimer = null;
244
+ }
239
245
  if (heartbeatController) {
240
246
  heartbeatController.abort();
241
247
  heartbeatController = null;
@@ -266,6 +272,7 @@ function startRunnerHeartbeatSupervisor() {
266
272
  JSON.stringify({ runId: config.runId, cadenceMs, handoffHeartbeatAgeMs: Math.max(0, Date.now() - heartbeatLastSuccessAtMs) }) +
267
273
  '\\n',
268
274
  );
275
+ heartbeatCadenceMs = cadenceMs;
269
276
  void runnerHeartbeatTick();
270
277
  heartbeatTimer = setInterval(runnerHeartbeatTick, cadenceMs);
271
278
  heartbeatTimer.unref?.();
@@ -341,11 +348,24 @@ async function runnerHeartbeatTick() {
341
348
  '[play-runner.heartbeat] watchdog_owner_rejected ' + JSON.stringify(diagnostic) + '\\n',
342
349
  );
343
350
  stopChild('runner_heartbeat_rejected', diagnostic);
351
+ return;
344
352
  }
345
353
  // Otherwise keep renewing. An unreachable or failing control plane is not
346
354
  // evidence that this run is over: the scheduler owns the liveness verdict
347
355
  // and confirms it only after it can accept heartbeats again. The runtime
348
356
  // limit, the terminal channel above, and sandbox cleanup bound this process.
357
+ // The lease is only a few cadences long, so one shed renewal must not wait
358
+ // a whole cadence. Only a 429 retries early: it says nothing happened and
359
+ // to try again, whereas retrying a failing store would add to its load.
360
+ // Jitter keeps a fleet shed together from retrying together, and the
361
+ // in-flight guard stops a retry overlapping a tick.
362
+ if (errorCode === 'http_429' && !heartbeatRetryTimer) {
363
+ heartbeatRetryTimer = setTimeout(() => {
364
+ heartbeatRetryTimer = null;
365
+ void runnerHeartbeatTick();
366
+ }, (heartbeatCadenceMs / 4) * (1 + Math.random() / 2));
367
+ heartbeatRetryTimer.unref?.();
368
+ }
349
369
  } finally {
350
370
  clearTimeout(timeout);
351
371
  if (heartbeatController === controller) heartbeatController = null;
@@ -58,7 +58,10 @@ import {
58
58
  isPlayRunRecoveryError,
59
59
  recoveryForDaytonaSandboxTransitionPending,
60
60
  } from '../../play-run-recovery-policy';
61
- import { stageDaytonaRunnerPayload } from './daytona-payload-transport';
61
+ import {
62
+ stageDaytonaRunnerPayload,
63
+ withRunnerSandboxProvider,
64
+ } from './daytona-payload-transport';
62
65
  import {
63
66
  DaytonaRunnerInitializationError,
64
67
  prepareDetachedDaytonaRunner,
@@ -819,6 +822,20 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
819
822
  * `{ exitCode: null, result: null }` and the attempt fails through the normal
820
823
  * engine retry/fencing path.
821
824
  */
825
+ /**
826
+ * Daytona states in which no process in the sandbox can still be running.
827
+ * `error` is left out: nothing here shows it means the sandbox stopped rather
828
+ * than that Daytona lost track of it, and a live run must not fail on that.
829
+ */
830
+ const DAYTONA_SANDBOX_STOPPED_STATES = new Set([
831
+ 'stopped',
832
+ 'stopping',
833
+ 'archived',
834
+ 'archiving',
835
+ 'destroyed',
836
+ 'destroying',
837
+ ]);
838
+
822
839
  export async function inspectDetachedDaytonaRunner(input: {
823
840
  sandboxId: string;
824
841
  sessionId: string;
@@ -836,6 +853,7 @@ export async function inspectDetachedDaytonaRunner(input: {
836
853
  | 'command_running'
837
854
  | 'result_missing'
838
855
  | 'command_inspection_failed'
856
+ | 'sandbox_stopped'
839
857
  | 'sandbox_missing'
840
858
  | 'sandbox_lookup_failed'
841
859
  | 'wrong_routing_domain';
@@ -882,6 +900,19 @@ export async function inspectDetachedDaytonaRunner(input: {
882
900
  error: error instanceof Error ? error.message : String(error),
883
901
  });
884
902
  }
903
+ const stopped = DAYTONA_SANDBOX_STOPPED_STATES.has(
904
+ String(sandbox.state ?? '').toLowerCase(),
905
+ );
906
+ if (!stopped && commandFound && exitCode === null) {
907
+ // A running command has no exit-code file to recover. Polling for one
908
+ // would hold a worker slot for the whole recovery budget, on every
909
+ // re-inspection of a run that is only quiet.
910
+ return {
911
+ exitCode,
912
+ result: null,
913
+ diagnosis: { stage: 'command_running', detail: null },
914
+ };
915
+ }
885
916
  // The full captured stdout file can scale with customer output. Metadata
886
917
  // time must not consume its existing independent file-recovery budget.
887
918
  const recoveryFiles = control.legacyOutputRecovery(sandbox);
@@ -896,13 +927,13 @@ export async function inspectDetachedDaytonaRunner(input: {
896
927
  return {
897
928
  exitCode,
898
929
  result: null,
899
- diagnosis: commandInspectionError
900
- ? {
901
- stage: 'command_inspection_failed',
902
- detail: commandInspectionError,
903
- }
904
- : commandFound && exitCode === null
905
- ? { stage: 'command_running', detail: null }
930
+ diagnosis: stopped
931
+ ? { stage: 'sandbox_stopped', detail: String(sandbox.state) }
932
+ : commandInspectionError
933
+ ? {
934
+ stage: 'command_inspection_failed',
935
+ detail: commandInspectionError,
936
+ }
906
937
  : { stage: 'result_missing', detail: null },
907
938
  };
908
939
  }
@@ -1491,10 +1522,14 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1491
1522
  const reportedSandboxIds = new Set<string>();
1492
1523
  const reportedSandboxEndTimes = new Map<string, number | null>();
1493
1524
  const runtimeResourceReportErrors = new Set<unknown>();
1494
- const reportRuntimeResource = async (acquired: AcquiredDaytonaSandbox) => {
1525
+ const reportRuntimeResource = async (
1526
+ acquired: AcquiredDaytonaSandbox,
1527
+ revalidate = false,
1528
+ ) => {
1495
1529
  const sandbox = acquired.sandbox;
1496
1530
  const billingEndedAt = acquired.billingEndedAt ?? null;
1497
1531
  if (
1532
+ !revalidate &&
1498
1533
  reportedSandboxIds.has(sandbox.id) &&
1499
1534
  reportedSandboxEndTimes.get(sandbox.id) === billingEndedAt
1500
1535
  ) {
@@ -1598,7 +1633,21 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1598
1633
  cancellationPromise,
1599
1634
  ]);
1600
1635
  activeAcquiredResource = acquiredSandbox;
1601
- sandboxCleanup.activate(acquiredSandbox);
1636
+ sandboxCleanup.activate({
1637
+ ...acquiredSandbox,
1638
+ deleteSandbox: () =>
1639
+ acquiredSandbox.deleteSandbox(
1640
+ callbacks?.onRuntimeResourceAcquired &&
1641
+ acquiredSandbox.sandboxCapacityLeaseId
1642
+ ? {
1643
+ // Bypass the local report cache: the scheduler must prove this
1644
+ // exact attempt still owns deletion before each mutating retry.
1645
+ authorizeRetry: () =>
1646
+ reportRuntimeResource(acquiredSandbox, true),
1647
+ }
1648
+ : undefined,
1649
+ ),
1650
+ });
1602
1651
  runtimeTiming.daytonaCreateMs =
1603
1652
  acquiredSandbox.billingStartedAt - startedAt;
1604
1653
  const sandbox = acquiredSandbox.sandbox;
@@ -1730,7 +1779,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1730
1779
  stageDaytonaRunnerPayload({
1731
1780
  sandbox: uploadScope.sandbox,
1732
1781
  bundlePromise,
1733
- config,
1782
+ config: withRunnerSandboxProvider(config, 'daytona'),
1734
1783
  workDir,
1735
1784
  startedAt,
1736
1785
  emitStage: (stage, extra) =>
@@ -1890,8 +1939,24 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1890
1939
  if (sandboxForAttempt) {
1891
1940
  activeAcquiredResource!.billingEndedAt = Date.now();
1892
1941
  await reportRetiringRuntimeResource(activeAcquiredResource!);
1893
- await sandboxCleanup.stashActiveSandboxForRetry();
1942
+ try {
1943
+ await sandboxCleanup.stashActiveSandboxForRetry();
1944
+ } catch (retirementError) {
1945
+ if (runtimeResourceReportErrors.has(retirementError))
1946
+ throw retirementError;
1947
+ // Preserve both causes without feeding a retryable startup
1948
+ // cause back into replacement admission. The cleanup manager
1949
+ // retains the active resource and its single retirement promise.
1950
+ const failure = new Error(
1951
+ `Daytona sandbox retirement unconfirmed: ${formatDaytonaError(retirementError)}. Original startup failure: ${formatDaytonaError(error)}`,
1952
+ { cause: error },
1953
+ );
1954
+ failure.name = 'DaytonaSandboxRetirementUnconfirmedError';
1955
+ Object.assign(failure, { retirementError });
1956
+ throw failure;
1957
+ }
1894
1958
  }
1959
+ throwIfCancellationRequested();
1895
1960
  acquiredSandboxPromise = sandboxLifecycle.createFreshSandbox();
1896
1961
  continue;
1897
1962
  }