deepline 0.3.247 → 0.3.248

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 (65) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +177 -13
  2. package/dist/bundling-sources/sdk/src/index.ts +8 -0
  3. package/dist/bundling-sources/sdk/src/plays/input-example.ts +40 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/runs/overview.ts +457 -0
  6. package/dist/bundling-sources/sdk/src/runs/receipts.ts +23 -0
  7. package/dist/bundling-sources/sdk/src/tool-calls.ts +21 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +16 -1
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +19 -1
  10. package/dist/bundling-sources/shared_libs/play-data-plane/column-names.ts +17 -3
  11. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +40 -8
  12. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +26 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +142 -14
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +6 -3
  16. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +26 -21
  17. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +23 -21
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +17 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +105 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +8 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +40 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +50 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +3 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +28 -1
  26. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +14 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +56 -16
  28. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +184 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +327 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +87 -30
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +24 -13
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +23 -6
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +76 -33
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +90 -35
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +16 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +62 -21
  37. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +368 -66
  38. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +22 -4
  39. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +105 -121
  40. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +1347 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +16 -19
  42. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +14 -6
  43. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +16 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +40 -0
  45. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +5 -2
  46. package/dist/bundling-sources/shared_libs/plays/dataset-summary.ts +11 -7
  47. package/dist/bundling-sources/shared_libs/plays/static-pipeline-storage-codec.ts +5 -2
  48. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +11 -2
  49. package/dist/cli/index.js +3504 -2368
  50. package/dist/cli/index.mjs +3188 -2052
  51. package/dist/{compiler-manifest-n8ZtPv2h.d.mts → compiler-manifest-DavmLa8y.d.mts} +1 -0
  52. package/dist/{compiler-manifest-n8ZtPv2h.d.ts → compiler-manifest-DavmLa8y.d.ts} +1 -0
  53. package/dist/index.d.mts +97 -5
  54. package/dist/index.d.ts +97 -5
  55. package/dist/index.js +135 -15
  56. package/dist/index.mjs +135 -15
  57. package/dist/install-integrity.json +9 -2
  58. package/dist/plays/bundle-play-file.d.mts +2 -2
  59. package/dist/plays/bundle-play-file.d.ts +2 -2
  60. package/dist/plays/bundle-play-file.mjs +1 -1
  61. package/dist/release.d.mts +1 -1
  62. package/dist/release.d.ts +1 -1
  63. package/dist/release.js +1 -1
  64. package/dist/release.mjs +1 -1
  65. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { type Daytona } from '@daytonaio/sdk';
1
+ import type { CreateSandboxFromSnapshotParams, Sandbox } from '@daytonaio/sdk';
2
+ import { DaytonaLaunchOutcomeUnknownError } from './daytona-launch-transport';
2
3
  import type {
3
4
  PlayRunnerExecutionConfig,
4
5
  PlayRunnerResult,
@@ -40,8 +41,18 @@ const DAYTONA_NETWORK_ALLOW_LIST_ENV = 'DEEPLINE_DAYTONA_NETWORK_ALLOW_LIST';
40
41
 
41
42
  export const DAYTONA_CANCELLED_ERROR = 'Daytona play runner cancelled';
42
43
 
43
- export type DaytonaClient = Pick<Daytona, 'create'> &
44
- Partial<Pick<Daytona, 'get'>>;
44
+ export type DaytonaClient = {
45
+ create(
46
+ params: CreateSandboxFromSnapshotParams,
47
+ options: { timeout: number },
48
+ ): Promise<Sandbox>;
49
+ get?(name: string): Promise<Sandbox>;
50
+ delete(sandbox: Pick<Sandbox, 'id'>, timeoutSeconds?: number): Promise<void>;
51
+ scope(
52
+ sandbox: Sandbox,
53
+ timeoutMs: number,
54
+ ): { sandbox: Sandbox; dispose(): void };
55
+ };
45
56
  export type DaytonaSandbox = Awaited<ReturnType<DaytonaClient['create']>>;
46
57
  export type DaytonaExecutionContext = PlayRunnerExecutionConfig['context'];
47
58
  export type DaytonaStageEmitter = (
@@ -55,6 +66,7 @@ type DaytonaCreateCallObserver = (
55
66
 
56
67
  export type AcquiredDaytonaSandbox = {
57
68
  sandbox: DaytonaSandbox;
69
+ deleteSandbox: () => Promise<void>;
58
70
  daytonaOrganizationId: string;
59
71
  billingStartedAt: number;
60
72
  billingEndedAt?: number;
@@ -125,6 +137,25 @@ export function resolveDaytonaSandboxAcquisitionUnavailableReason(
125
137
  return null;
126
138
  }
127
139
 
140
+ function waitForAcquisitionRetry(
141
+ ms: number,
142
+ signal?: AbortSignal,
143
+ ): Promise<void> {
144
+ signal?.throwIfAborted();
145
+ return new Promise((resolve, reject) => {
146
+ const abort = () => {
147
+ clearTimeout(timer);
148
+ reject(signal?.reason);
149
+ };
150
+ const timer = setTimeout(() => {
151
+ signal?.removeEventListener('abort', abort);
152
+ resolve();
153
+ }, ms);
154
+ signal?.addEventListener('abort', abort, { once: true });
155
+ if (signal?.aborted) abort();
156
+ });
157
+ }
158
+
128
159
  function isDaytonaSandboxStartTimeout(error: string): boolean {
129
160
  return /Failed to create and start sandbox within \d+ seconds\. Operation timed out\./.test(
130
161
  error,
@@ -161,11 +192,12 @@ function daytonaCreateErrorClass(
161
192
  async function rejectAcquiredSandbox(
162
193
  sandbox: DaytonaSandbox,
163
194
  reason: string,
164
- reservation?: { leaseId: string },
165
- releaseSandboxCapacity?: (leaseId: string) => Promise<void>,
195
+ reservation: { leaseId: string } | undefined,
196
+ releaseSandboxCapacity: ((leaseId: string) => Promise<void>) | undefined,
197
+ deleteSandbox: () => Promise<void>,
166
198
  ): Promise<never> {
167
199
  try {
168
- await sandbox.delete(30);
200
+ await deleteSandbox();
169
201
  } catch (error) {
170
202
  const cleanupError = error instanceof Error ? error.message : String(error);
171
203
  throw new Error(
@@ -346,20 +378,22 @@ type TimedOutDaytonaSandboxReconciliation =
346
378
  async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
347
379
  daytona: DaytonaClient;
348
380
  sandboxName: string;
381
+ cancellationSignal?: AbortSignal;
349
382
  }): Promise<TimedOutDaytonaSandboxReconciliation> {
350
383
  if (!input.daytona.get) return { kind: 'not_found' };
351
384
  let lastDeleteConflict: unknown = null;
352
385
  for (const delayMs of DAYTONA_TIMED_OUT_CREATE_RECONCILE_DELAYS_MS) {
386
+ input.cancellationSignal?.throwIfAborted();
353
387
  if (delayMs > 0) {
354
- await new Promise((resolve) => setTimeout(resolve, delayMs));
388
+ await waitForAcquisitionRetry(delayMs, input.cancellationSignal);
355
389
  }
356
390
  let sandbox: DaytonaSandbox;
357
391
  try {
358
392
  sandbox = await input.daytona.get(input.sandboxName);
359
393
  } catch (error) {
360
- // A confirmed absence cannot become a cleanup-addressable resource on a
361
- // later read. Stop immediately; only uncertain lookup failures merit
362
- // the bounded retry window below.
394
+ // A current absence is not proof that the sent create cannot commit.
395
+ // Return it to the durable operation owner without another create or
396
+ // releasing capacity; later recovery re-reads the same identity.
363
397
  if (isDaytonaSandboxNotFound(error)) return { kind: 'not_found' };
364
398
  continue;
365
399
  }
@@ -372,7 +406,7 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
372
406
  return { kind: 'started', sandbox };
373
407
  }
374
408
  try {
375
- await sandbox.delete(30);
409
+ await input.daytona.delete(sandbox, 30);
376
410
  } catch (error) {
377
411
  // Daytona can make the named sandbox lookup-addressable before its
378
412
  // timed-out create transition has settled. That exact conflict is not a
@@ -416,9 +450,11 @@ async function createRetriedOneShotDaytonaSandbox(input: {
416
450
  releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
417
451
  nextProviderAttempt: () => number;
418
452
  startedAt: number;
453
+ cancellationSignal?: AbortSignal;
419
454
  }): Promise<DaytonaCreateResult> {
420
455
  const errors: string[] = [];
421
456
  for (const [index, delayMs] of DAYTONA_CREATE_RETRY_DELAYS_MS.entries()) {
457
+ input.cancellationSignal?.throwIfAborted();
422
458
  const attempt = input.nextProviderAttempt();
423
459
  if (delayMs > 0) {
424
460
  input.emitStage('create:retry', {
@@ -427,7 +463,7 @@ async function createRetriedOneShotDaytonaSandbox(input: {
427
463
  reason: 'bounded_backoff',
428
464
  delayMs,
429
465
  });
430
- await new Promise((resolve) => setTimeout(resolve, delayMs));
466
+ await waitForAcquisitionRetry(delayMs, input.cancellationSignal);
431
467
  }
432
468
  const attemptStartedAt = Date.now();
433
469
  await input.observeCreateCall?.({
@@ -463,11 +499,9 @@ async function createRetriedOneShotDaytonaSandbox(input: {
463
499
  if (!isDaytonaSandboxNotFound(error)) {
464
500
  throw new DaytonaSandboxTransitionPendingError(sandboxName);
465
501
  }
466
- await input.releaseSandboxCapacity?.(reservation.leaseId);
467
- throw new DaytonaSandboxAcquisitionUnavailableError(
468
- 'daytona_sandbox_start_timeout',
469
- `Timed-out Daytona sandbox ${sandboxName} was confirmed absent before resume.`,
470
- );
502
+ // A lookup 404 does not prove that an earlier sent create cannot still
503
+ // commit. Keep the durable identity reserved; never hedge its outcome.
504
+ throw new DaytonaSandboxTransitionPendingError(sandboxName);
471
505
  }
472
506
  }
473
507
  let sandbox: DaytonaSandbox;
@@ -507,12 +541,16 @@ async function createRetriedOneShotDaytonaSandbox(input: {
507
541
  });
508
542
  const immediateFallbackReason =
509
543
  resolveDaytonaSandboxAcquisitionUnavailableReason([message]);
510
- if (isDaytonaSandboxStartTimeout(message)) {
544
+ if (
545
+ error instanceof DaytonaLaunchOutcomeUnknownError ||
546
+ isDaytonaSandboxStartTimeout(message)
547
+ ) {
511
548
  let reconciliation: TimedOutDaytonaSandboxReconciliation;
512
549
  try {
513
550
  reconciliation = await reconcileAndDeleteTimedOutDaytonaSandbox({
514
551
  daytona: input.daytona,
515
552
  sandboxName,
553
+ cancellationSignal: input.cancellationSignal,
516
554
  });
517
555
  } catch (reconciliationError) {
518
556
  await recordCreateFailure();
@@ -571,6 +609,9 @@ async function createRetriedOneShotDaytonaSandbox(input: {
571
609
  `${message} Timed-out Daytona sandbox was reconciled and deleted before fallback.`,
572
610
  );
573
611
  }
612
+ if (reservation?.providerOperationToken) {
613
+ throw new DaytonaSandboxTransitionPendingError(sandboxName);
614
+ }
574
615
  throw new Error(
575
616
  `${message} Daytona did not return a cleanup-addressable sandbox identity; Modal fallback suppressed. The named ephemeral sandbox retains Daytona's auto-delete-on-stop backstop.`,
576
617
  { cause: error },
@@ -645,6 +686,7 @@ async function acquireOneShotDaytonaSandbox(input: {
645
686
  nextProviderAttempt: () => number;
646
687
  startedAt: number;
647
688
  startAdmission?: DaytonaSandboxStartAdmission;
689
+ cancellationSignal?: AbortSignal;
648
690
  }): Promise<AcquiredDaytonaSandbox> {
649
691
  const limits = validatePlaySandboxRuntimeLimits(
650
692
  input.context.sandboxRuntimeLimits ?? {
@@ -653,8 +695,9 @@ async function acquireOneShotDaytonaSandbox(input: {
653
695
  );
654
696
  input.emitStage('create:start');
655
697
  const result = input.startAdmission
656
- ? await input.startAdmission.run(() =>
657
- createRetriedOneShotDaytonaSandbox(input),
698
+ ? await input.startAdmission.run(
699
+ () => createRetriedOneShotDaytonaSandbox(input),
700
+ input.cancellationSignal,
658
701
  )
659
702
  : await createRetriedOneShotDaytonaSandbox(input);
660
703
  // The runner's egress probe measures the sandbox-to-estate round trip; the
@@ -682,6 +725,7 @@ async function acquireOneShotDaytonaSandbox(input: {
682
725
  ? { leaseId: result.sandboxCapacityLeaseId }
683
726
  : undefined,
684
727
  input.releaseSandboxCapacity,
728
+ () => input.daytona.delete(result.sandbox, 30),
685
729
  );
686
730
  }
687
731
  const configuredOrganizationId =
@@ -699,6 +743,7 @@ async function acquireOneShotDaytonaSandbox(input: {
699
743
  ? { leaseId: result.sandboxCapacityLeaseId }
700
744
  : undefined,
701
745
  input.releaseSandboxCapacity,
746
+ () => input.daytona.delete(result.sandbox, 30),
702
747
  );
703
748
  }
704
749
  let lookupOrganizationId: string | null = null;
@@ -725,6 +770,7 @@ async function acquireOneShotDaytonaSandbox(input: {
725
770
  ? { leaseId: result.sandboxCapacityLeaseId }
726
771
  : undefined,
727
772
  input.releaseSandboxCapacity,
773
+ () => input.daytona.delete(result.sandbox, 30),
728
774
  );
729
775
  }
730
776
  const billingStartedAt = Date.now();
@@ -745,6 +791,7 @@ async function acquireOneShotDaytonaSandbox(input: {
745
791
  });
746
792
  return {
747
793
  sandbox,
794
+ deleteSandbox: () => input.daytona.delete(sandbox, 30),
748
795
  daytonaOrganizationId,
749
796
  billingStartedAt,
750
797
  ...(result.sandboxCapacityLeaseId
@@ -765,6 +812,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
765
812
  }>;
766
813
  releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
767
814
  startAdmission?: DaytonaSandboxStartAdmission;
815
+ cancellationSignal?: AbortSignal;
768
816
  startedAt?: number;
769
817
  }): OneShotDaytonaSandboxLifecycle {
770
818
  const orgId = validateDaytonaExecutionContext(input.context);
@@ -783,6 +831,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
783
831
  reserveSandboxCapacity: input.reserveSandboxCapacity,
784
832
  releaseSandboxCapacity: input.releaseSandboxCapacity,
785
833
  startAdmission: input.startAdmission,
834
+ cancellationSignal: input.cancellationSignal,
786
835
  nextProviderAttempt: () => {
787
836
  providerAttempt += 1;
788
837
  return providerAttempt;
@@ -812,7 +861,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
812
861
  disposed = true;
813
862
  try {
814
863
  const acquired = await latestAcquiredSandboxPromise;
815
- await acquired.sandbox.delete(30);
864
+ await acquired.deleteSandbox();
816
865
  // `dispose` has provider-confirmed that the sandbox is gone. This is
817
866
  // normally the narrow cancellation window before the scheduler has
818
867
  // durably recorded the resource, so no cleanup job will release an
@@ -846,7 +895,7 @@ export function createDaytonaSandboxCleanupManager(): {
846
895
  activate(acquired: AcquiredDaytonaSandbox): void;
847
896
  currentSandbox(): DaytonaSandbox | null;
848
897
  cleanupActiveSandboxForCancellation(): boolean;
849
- stashActiveSandboxForRetry(): void;
898
+ stashActiveSandboxForRetry(): Promise<void>;
850
899
  withCleanup(
851
900
  result: PlayRunnerResult,
852
901
  input: { cancellationCleanupStarted: boolean },
@@ -855,6 +904,11 @@ export function createDaytonaSandboxCleanupManager(): {
855
904
  let sandbox: DaytonaSandbox | null = null;
856
905
  let sandboxBillingStartedAt: number | null = null;
857
906
  const deletedSandboxIds = new Set<string>();
907
+ let deleteActive: (() => Promise<void>) | null = null;
908
+ let retirement: Promise<void> | null = null;
909
+ // Cancellation, terminal cleanup, and retry retirement share the same
910
+ // provider operation. A rejected/ambiguous delete still blocks replacement.
911
+ const retireActive = () => (retirement ??= deleteActive!());
858
912
 
859
913
  const deleteSandboxOnce = (input: {
860
914
  reason: string;
@@ -866,7 +920,7 @@ export function createDaytonaSandboxCleanupManager(): {
866
920
  }
867
921
  deletedSandboxIds.add(target.id);
868
922
  const billingStartedAt = sandboxBillingStartedAt;
869
- void target.delete(30).then(
923
+ void retireActive().then(
870
924
  () => {
871
925
  console.info('[play-runner.daytona.cleanup_done]', {
872
926
  reason: input.reason,
@@ -889,6 +943,8 @@ export function createDaytonaSandboxCleanupManager(): {
889
943
  return {
890
944
  activate(acquired) {
891
945
  sandbox = acquired.sandbox;
946
+ deleteActive = acquired.deleteSandbox;
947
+ retirement = null;
892
948
  sandboxBillingStartedAt = acquired.billingStartedAt;
893
949
  },
894
950
  currentSandbox() {
@@ -900,14 +956,15 @@ export function createDaytonaSandboxCleanupManager(): {
900
956
  sandboxToDelete: sandbox,
901
957
  });
902
958
  },
903
- stashActiveSandboxForRetry() {
904
- // The retired sandbox is dead — a fresh one replaces it for the retry.
905
- // Delete NOW instead of deferring to a consumer that never existed.
906
- deleteSandboxOnce({
907
- reason: 'retired_for_retry',
908
- sandboxToDelete: sandbox,
909
- });
959
+ async stashActiveSandboxForRetry() {
960
+ if (!sandbox || !deleteActive) return;
961
+ // A response lost after command acceptance can leave a runner alive.
962
+ // No replacement may start until provider-confirmed retirement.
963
+ const target = sandbox;
964
+ await retireActive();
965
+ deletedSandboxIds.add(target.id);
910
966
  sandbox = null;
967
+ deleteActive = null;
911
968
  sandboxBillingStartedAt = null;
912
969
  },
913
970
  withCleanup(result, input) {
@@ -358,6 +358,7 @@ export function prepareDetachedDaytonaRunner(input: {
358
358
  sessionId: string;
359
359
  readiness: DetachedRunnerReadinessPort;
360
360
  cancellation?: Promise<never>;
361
+ runControl<T>(operation: (sandbox: DaytonaSandbox) => Promise<T>): Promise<T>;
361
362
  /** Invoked immediately after Daytona accepts the detached command. */
362
363
  onCommandAccepted?: (
363
364
  state: Extract<DetachedDaytonaRunnerState, { phase: 'command_accepted' }>,
@@ -368,8 +369,8 @@ export function prepareDetachedDaytonaRunner(input: {
368
369
  ) => Promise<void> | void;
369
370
  }): DetachedDaytonaRunnerSupervisor {
370
371
  const cancellation = input.cancellation ?? new Promise<never>(() => {});
371
- const sessionCreation = input.sandbox.process
372
- .createSession(input.sessionId)
372
+ const sessionCreation = input
373
+ .runControl((sandbox) => sandbox.process.createSession(input.sessionId))
373
374
  .then(
374
375
  () => ({ ok: true as const }),
375
376
  (error: unknown) => ({ ok: false as const, error }),
@@ -399,10 +400,12 @@ export function prepareDetachedDaytonaRunner(input: {
399
400
  const baseline = await Promise.race([readinessBaseline, cancellation]);
400
401
  if (baseline.ok === false) throw baseline.error;
401
402
  const response = await Promise.race([
402
- input.sandbox.process.executeSessionCommand(input.sessionId, {
403
- command,
404
- runAsync: true,
405
- }),
403
+ input.runControl((sandbox) =>
404
+ sandbox.process.executeSessionCommand(input.sessionId, {
405
+ command,
406
+ runAsync: true,
407
+ }),
408
+ ),
406
409
  cancellation,
407
410
  ]);
408
411
  const cmdId = response?.cmdId;
@@ -427,6 +430,7 @@ export function prepareDetachedDaytonaRunner(input: {
427
430
  startupDiagnosticPath,
428
431
  readiness: input.readiness,
429
432
  baselineHeartbeatAt: state.baselineHeartbeatAt,
433
+ runControl: input.runControl,
430
434
  }),
431
435
  cancellation,
432
436
  ]);
@@ -452,6 +456,9 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
452
456
  startupDiagnosticPath?: string;
453
457
  readiness: DetachedRunnerReadinessPort;
454
458
  baselineHeartbeatAt: string | null;
459
+ runControl?: <T>(
460
+ operation: (sandbox: DaytonaSandbox) => Promise<T>,
461
+ ) => Promise<T>;
455
462
  timeoutMs?: number;
456
463
  pollMs?: number;
457
464
  now?: () => number;
@@ -488,13 +495,17 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
488
495
  retryDelayMs = Math.min(1_000, retryDelayMs * 2);
489
496
  }
490
497
 
491
- const diagnostic = await inspectDetachedDaytonaStartup({
492
- sandbox: input.sandbox,
493
- sessionId: input.sessionId,
494
- cmdId: input.cmdId,
495
- exitCodePath: input.exitCodePath,
496
- startupDiagnosticPath: input.startupDiagnosticPath,
497
- });
498
+ const inspect = (sandbox: DaytonaSandbox) =>
499
+ inspectDetachedDaytonaStartup({
500
+ sandbox,
501
+ sessionId: input.sessionId,
502
+ cmdId: input.cmdId,
503
+ exitCodePath: input.exitCodePath,
504
+ startupDiagnosticPath: input.startupDiagnosticPath,
505
+ });
506
+ const diagnostic = await (input.runControl
507
+ ? input.runControl(inspect)
508
+ : inspect(input.sandbox));
498
509
  diagnostic.schedulerReadinessObserved = false;
499
510
  diagnostic.schedulerReadinessResponses = schedulerReadinessResponses;
500
511
  const oomIndicated =
@@ -1,7 +1,7 @@
1
1
  import { RUNTIME_CAPACITY_POLICY } from '../../runtime-capacity-policy';
2
2
 
3
3
  export type DaytonaSandboxStartAdmission = {
4
- run<T>(operation: () => Promise<T>): Promise<T>;
4
+ run<T>(operation: () => Promise<T>, signal?: AbortSignal): Promise<T>;
5
5
  };
6
6
 
7
7
  /**
@@ -13,9 +13,8 @@ export type DaytonaSandboxStartAdmission = {
13
13
  * is still transitioning, recreating the unsafe overlap this gate prevents.
14
14
  */
15
15
  export function createDaytonaSandboxStartAdmission(
16
- concurrency =
17
- RUNTIME_CAPACITY_POLICY.sandboxAcquisition
18
- .daytonaConcurrentStartsPerProcess,
16
+ concurrency = RUNTIME_CAPACITY_POLICY.sandboxAcquisition
17
+ .daytonaConcurrentStartsPerProcess,
19
18
  ): DaytonaSandboxStartAdmission {
20
19
  if (!Number.isInteger(concurrency) || concurrency <= 0) {
21
20
  throw new Error(
@@ -43,7 +42,11 @@ export function createDaytonaSandboxStartAdmission(
43
42
  };
44
43
 
45
44
  return {
46
- async run<T>(operation: () => Promise<T>): Promise<T> {
45
+ async run<T>(
46
+ operation: () => Promise<T>,
47
+ signal?: AbortSignal,
48
+ ): Promise<T> {
49
+ signal?.throwIfAborted();
47
50
  // Preserve eager prepare semantics when capacity is free: the provider
48
51
  // create begins in this turn, just as it did before admission existed.
49
52
  // Only contended callers cross an asynchronous queue boundary.
@@ -53,8 +56,22 @@ export function createDaytonaSandboxStartAdmission(
53
56
  active += 1;
54
57
  return createRelease();
55
58
  })()
56
- : await new Promise<() => void>((resolve) => waiters.push(resolve));
59
+ : await new Promise<() => void>((resolve, reject) => {
60
+ const accept = (release: () => void) => {
61
+ signal?.removeEventListener('abort', abort);
62
+ resolve(release);
63
+ };
64
+ const abort = () => {
65
+ const index = waiters.indexOf(accept);
66
+ if (index >= 0) waiters.splice(index, 1);
67
+ reject(signal?.reason);
68
+ };
69
+ waiters.push(accept);
70
+ signal?.addEventListener('abort', abort, { once: true });
71
+ if (signal?.aborted) abort();
72
+ });
57
73
  try {
74
+ signal?.throwIfAborted();
58
75
  return await operation();
59
76
  } finally {
60
77
  release();