deepline 0.1.206 → 0.1.207

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 (29) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +25 -16
  2. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  3. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +96 -12
  4. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +51 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +16 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/child-run-lifecycle.ts +70 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +321 -175
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +13 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +16 -4
  10. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +161 -81
  11. package/dist/bundling-sources/shared_libs/play-runtime/governor/coordinator-rate-state-backend.ts +26 -9
  12. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +109 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +1 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +7 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/run-execution-scope.ts +256 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +17 -5
  17. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +103 -18
  18. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +8 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver-pg.ts +5 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +74 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -12
  23. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -1
  24. package/dist/bundling-sources/shared_libs/runtime-env.ts +55 -0
  25. package/dist/cli/index.js +27 -6
  26. package/dist/cli/index.mjs +27 -6
  27. package/dist/index.js +2 -2
  28. package/dist/index.mjs +2 -2
  29. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ import {
2
+ isRuntimeExecutionCapability,
3
+ type RuntimeExecutionCapability,
4
+ } from './execution-capabilities';
5
+
6
+ export type RunExecutionPlacementKind =
7
+ | 'root'
8
+ | 'inline_child'
9
+ | 'scheduled_child';
10
+
11
+ export type RunExecutionAuthorityInput = {
12
+ readonly orgId: string;
13
+ readonly workflowId?: string;
14
+ readonly billingRunId?: string | null;
15
+ readonly maxCreditsPerRun?: number | null;
16
+ readonly integrationMode?: 'live' | 'eval_stub' | 'fixture' | null;
17
+ readonly synthetic?: boolean;
18
+ readonly capabilities: readonly RuntimeExecutionCapability[];
19
+ };
20
+
21
+ export type RunExecutionScope = Readonly<{
22
+ logical: Readonly<{
23
+ runId: string;
24
+ playId: string;
25
+ parentRunId: string | null;
26
+ parentPlayId: string | null;
27
+ rootRunId: string;
28
+ rootPlayId: string;
29
+ }>;
30
+ receipt: Readonly<{
31
+ ownerRunId: string;
32
+ ownerAttempt: number;
33
+ namespace: string;
34
+ }>;
35
+ placement: Readonly<{
36
+ kind: RunExecutionPlacementKind;
37
+ executorRunId: string;
38
+ executorPlayId: string;
39
+ }>;
40
+ /** Persistable runtime authority facts. Bearer tokens and secret names do not belong here. */
41
+ authority: Readonly<{
42
+ orgId: string;
43
+ workflowId: string;
44
+ runId: string;
45
+ playId: string;
46
+ billingRunId: string | null;
47
+ maxCreditsPerRun: number | null;
48
+ integrationMode: 'live' | 'eval_stub' | 'fixture' | null;
49
+ synthetic: boolean;
50
+ capabilities: readonly RuntimeExecutionCapability[];
51
+ }>;
52
+ }>;
53
+
54
+ export type RootRunExecutionScopeInput = {
55
+ readonly runId: string;
56
+ readonly playId: string;
57
+ readonly attempt: number;
58
+ readonly receiptNamespace: string;
59
+ readonly authority: RunExecutionAuthorityInput;
60
+ };
61
+
62
+ export type ChildRunExecutionScopeInput =
63
+ | {
64
+ readonly placement: 'inline';
65
+ readonly runId: string;
66
+ readonly playId: string;
67
+ readonly receiptNamespace: string;
68
+ }
69
+ | {
70
+ readonly placement: 'scheduled';
71
+ readonly runId: string;
72
+ readonly playId: string;
73
+ readonly attempt: number;
74
+ readonly receiptNamespace: string;
75
+ readonly authority?: Partial<Omit<RunExecutionAuthorityInput, 'orgId'>>;
76
+ };
77
+
78
+ function requireIdentity(value: string, field: string): string {
79
+ if (
80
+ typeof value !== 'string' ||
81
+ value.length === 0 ||
82
+ value !== value.trim()
83
+ ) {
84
+ throw new Error(`${field} must be a non-empty, trimmed string.`);
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function requireAttempt(value: number, field: string): number {
90
+ if (!Number.isSafeInteger(value) || value < 0) {
91
+ throw new Error(`${field} must be a non-negative safe integer.`);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ function requireMaxCredits(value: number | null | undefined): number | null {
97
+ if (value == null) return null;
98
+ if (!Number.isFinite(value) || value < 0) {
99
+ throw new Error(
100
+ 'authority.maxCreditsPerRun must be a non-negative number.',
101
+ );
102
+ }
103
+ return value;
104
+ }
105
+
106
+ function requireCapabilities(
107
+ values: readonly RuntimeExecutionCapability[],
108
+ ): readonly RuntimeExecutionCapability[] {
109
+ if (!Array.isArray(values) || values.length === 0) {
110
+ throw new Error(
111
+ 'authority.capabilities must contain at least one capability.',
112
+ );
113
+ }
114
+ const capabilities = [...new Set(values)];
115
+ if (!capabilities.every(isRuntimeExecutionCapability)) {
116
+ throw new Error('authority.capabilities contains an unknown capability.');
117
+ }
118
+ return Object.freeze(capabilities.sort());
119
+ }
120
+
121
+ function authorityMetadata(input: {
122
+ runId: string;
123
+ playId: string;
124
+ orgId: string;
125
+ authority: Omit<RunExecutionAuthorityInput, 'orgId'>;
126
+ }): RunExecutionScope['authority'] {
127
+ return Object.freeze({
128
+ orgId: requireIdentity(input.orgId, 'authority.orgId'),
129
+ workflowId: requireIdentity(
130
+ input.authority.workflowId ?? input.runId,
131
+ 'authority.workflowId',
132
+ ),
133
+ runId: input.runId,
134
+ playId: input.playId,
135
+ billingRunId:
136
+ input.authority.billingRunId == null
137
+ ? null
138
+ : requireIdentity(
139
+ input.authority.billingRunId,
140
+ 'authority.billingRunId',
141
+ ),
142
+ maxCreditsPerRun: requireMaxCredits(input.authority.maxCreditsPerRun),
143
+ integrationMode: input.authority.integrationMode ?? null,
144
+ synthetic: input.authority.synthetic === true,
145
+ capabilities: requireCapabilities(input.authority.capabilities),
146
+ });
147
+ }
148
+
149
+ function freezeScope(input: RunExecutionScope): RunExecutionScope {
150
+ return Object.freeze({
151
+ logical: Object.freeze(input.logical),
152
+ receipt: Object.freeze(input.receipt),
153
+ placement: Object.freeze(input.placement),
154
+ authority: input.authority,
155
+ });
156
+ }
157
+
158
+ export function createRootRunExecutionScope(
159
+ input: RootRunExecutionScopeInput,
160
+ ): RunExecutionScope {
161
+ const runId = requireIdentity(input.runId, 'runId');
162
+ const playId = requireIdentity(input.playId, 'playId');
163
+ const attempt = requireAttempt(input.attempt, 'attempt');
164
+ const namespace = requireIdentity(input.receiptNamespace, 'receiptNamespace');
165
+
166
+ return freezeScope({
167
+ logical: {
168
+ runId,
169
+ playId,
170
+ parentRunId: null,
171
+ parentPlayId: null,
172
+ rootRunId: runId,
173
+ rootPlayId: playId,
174
+ },
175
+ receipt: { ownerRunId: runId, ownerAttempt: attempt, namespace },
176
+ placement: { kind: 'root', executorRunId: runId, executorPlayId: playId },
177
+ authority: authorityMetadata({
178
+ runId,
179
+ playId,
180
+ orgId: input.authority.orgId,
181
+ authority: input.authority,
182
+ }),
183
+ });
184
+ }
185
+
186
+ export function deriveChildRunExecutionScope(
187
+ parent: RunExecutionScope,
188
+ input: ChildRunExecutionScopeInput,
189
+ ): RunExecutionScope {
190
+ const runId = requireIdentity(input.runId, 'runId');
191
+ const playId = requireIdentity(input.playId, 'playId');
192
+ const namespace = requireIdentity(input.receiptNamespace, 'receiptNamespace');
193
+ if (runId === parent.logical.runId) {
194
+ throw new Error('A child runId must differ from its parent runId.');
195
+ }
196
+
197
+ const logical: RunExecutionScope['logical'] = {
198
+ runId,
199
+ playId,
200
+ parentRunId: parent.logical.runId,
201
+ parentPlayId: parent.logical.playId,
202
+ rootRunId: parent.logical.rootRunId,
203
+ rootPlayId: parent.logical.rootPlayId,
204
+ };
205
+
206
+ if (input.placement === 'inline') {
207
+ return freezeScope({
208
+ logical,
209
+ receipt: {
210
+ ownerRunId: parent.receipt.ownerRunId,
211
+ ownerAttempt: parent.receipt.ownerAttempt,
212
+ namespace,
213
+ },
214
+ placement: {
215
+ kind: 'inline_child',
216
+ executorRunId: parent.placement.executorRunId,
217
+ executorPlayId: parent.placement.executorPlayId,
218
+ },
219
+ authority: parent.authority,
220
+ });
221
+ }
222
+
223
+ const attempt = requireAttempt(input.attempt, 'attempt');
224
+ return freezeScope({
225
+ logical,
226
+ receipt: { ownerRunId: runId, ownerAttempt: attempt, namespace },
227
+ placement: {
228
+ kind: 'scheduled_child',
229
+ executorRunId: runId,
230
+ executorPlayId: playId,
231
+ },
232
+ authority: authorityMetadata({
233
+ runId,
234
+ playId,
235
+ orgId: parent.authority.orgId,
236
+ authority: {
237
+ workflowId: input.authority?.workflowId,
238
+ billingRunId:
239
+ input.authority?.billingRunId === undefined
240
+ ? parent.authority.billingRunId
241
+ : input.authority.billingRunId,
242
+ maxCreditsPerRun:
243
+ input.authority?.maxCreditsPerRun === undefined
244
+ ? parent.authority.maxCreditsPerRun
245
+ : input.authority.maxCreditsPerRun,
246
+ integrationMode:
247
+ input.authority?.integrationMode === undefined
248
+ ? parent.authority.integrationMode
249
+ : input.authority.integrationMode,
250
+ synthetic: input.authority?.synthetic ?? parent.authority.synthetic,
251
+ capabilities:
252
+ input.authority?.capabilities ?? parent.authority.capabilities,
253
+ },
254
+ }),
255
+ });
256
+ }
@@ -39,12 +39,15 @@ export type DaytonaStageEmitter = (
39
39
  export type AcquiredDaytonaSandbox = {
40
40
  sandbox: DaytonaSandbox;
41
41
  billingStartedAt: number;
42
+ billingEndedAt?: number;
42
43
  };
43
44
 
44
45
  export type OneShotDaytonaSandboxLifecycle = {
45
46
  startedAt: number;
46
47
  acquiredSandboxPromise: Promise<AcquiredDaytonaSandbox>;
47
48
  createFreshSandbox: () => Promise<AcquiredDaytonaSandbox>;
49
+ acquiredSandboxes: () => readonly AcquiredDaytonaSandbox[];
50
+ settlePendingCreates: () => Promise<void>;
48
51
  dispose: () => Promise<void>;
49
52
  };
50
53
 
@@ -269,8 +272,9 @@ async function acquireOneShotDaytonaSandbox(input: {
269
272
  );
270
273
  }
271
274
  const billingStartedAt = Date.now();
275
+ const sandbox = result.sandbox;
272
276
  input.emitStage('create:done', {
273
- sandboxId: result.sandbox.id,
277
+ sandboxId: sandbox.id,
274
278
  attempt: result.attempt,
275
279
  elapsedMs: billingStartedAt - input.startedAt,
276
280
  attemptElapsedMs: result.attemptElapsedMs,
@@ -278,10 +282,7 @@ async function acquireOneShotDaytonaSandbox(input: {
278
282
  memoryGiB: granted.memoryGiB,
279
283
  diskGiB: granted.diskGiB,
280
284
  });
281
- return {
282
- sandbox: result.sandbox,
283
- billingStartedAt,
284
- };
285
+ return { sandbox, billingStartedAt };
285
286
  }
286
287
 
287
288
  export function createOneShotDaytonaSandboxLifecycle(input: {
@@ -293,6 +294,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
293
294
  const orgId = validateDaytonaExecutionContext(input.context);
294
295
  const startedAt = input.startedAt ?? Date.now();
295
296
  let disposed = false;
297
+ const acquiredSandboxes = new Map<string, AcquiredDaytonaSandbox>();
296
298
  let latestAcquiredSandboxPromise: Promise<AcquiredDaytonaSandbox>;
297
299
  const createFreshSandbox = () => {
298
300
  latestAcquiredSandboxPromise = acquireOneShotDaytonaSandbox({
@@ -301,6 +303,9 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
301
303
  context: input.context,
302
304
  emitStage: input.emitStage,
303
305
  startedAt,
306
+ }).then((acquired) => {
307
+ acquiredSandboxes.set(acquired.sandbox.id, acquired);
308
+ return acquired;
304
309
  });
305
310
  return latestAcquiredSandboxPromise;
306
311
  };
@@ -310,6 +315,13 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
310
315
  startedAt,
311
316
  acquiredSandboxPromise,
312
317
  createFreshSandbox,
318
+ acquiredSandboxes: () => [...acquiredSandboxes.values()],
319
+ settlePendingCreates: async () => {
320
+ await latestAcquiredSandboxPromise.then(
321
+ () => undefined,
322
+ () => undefined,
323
+ );
324
+ },
313
325
  dispose: async () => {
314
326
  if (disposed) return;
315
327
  disposed = true;
@@ -4,6 +4,7 @@ import type {
4
4
  PlayRunnerPreparedExecution,
5
5
  PlayRunnerPrepareInput,
6
6
  } from '../types';
7
+ import { RuntimeResourceFenceLostError } from '../types';
7
8
  import { buildPlayRunnerBundle } from '../bundle';
8
9
  import {
9
10
  findPlayRunnerResult,
@@ -113,6 +114,8 @@ export async function withDaytonaOperationDeadline<T>(input: {
113
114
  }
114
115
  const RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN =
115
116
  /\bRuntime Postgres\b.*\b(connection timed out|connect timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|Connection terminated|Connection ended unexpectedly)\b/i;
117
+ const RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN =
118
+ /AppRuntimeApiTransportError: App runtime API transport exhausted action=(?:get|claim|mark|complete|fail|heartbeat|release|skip)_runtime_step_receipts?/i;
116
119
  const RUNTIME_API_TRANSPORT_RETRY_PATTERN =
117
120
  /\bRuntime API request to .+ failed before receiving a response:\s*(?:fetch failed|connection (?:terminated|timed out|closed|reset)|ECONNRESET|ETIMEDOUT|ECONNREFUSED)\b/i;
118
121
  const DAYTONA_INFRASTRUCTURE_RETRY_PATTERN =
@@ -498,11 +501,18 @@ function createDaytonaFailedResult(input: {
498
501
 
499
502
  function retryableRuntimeInitializationFailureReason(
500
503
  result: PlayRunnerResult,
501
- ): 'runtime_postgres_connect' | 'runtime_api_transport' | null {
504
+ ):
505
+ | 'runtime_postgres_connect'
506
+ | 'runtime_api_transport'
507
+ | 'runtime_receipt_gateway_transport'
508
+ | null {
502
509
  if (result.status !== 'failed') return null;
503
510
  if (RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN.test(result.error)) {
504
511
  return 'runtime_postgres_connect';
505
512
  }
513
+ if (RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN.test(result.error)) {
514
+ return 'runtime_receipt_gateway_transport';
515
+ }
506
516
  const rowsProcessed = Number(result.stats?.rowsProcessed ?? 0);
507
517
  if (
508
518
  rowsProcessed === 0 &&
@@ -617,13 +627,69 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
617
627
  };
618
628
  let cancellationRequested = false;
619
629
  let cancellationCleanupStarted = false;
630
+ let activeAcquiredResource: {
631
+ sandbox: DaytonaSandbox;
632
+ billingStartedAt: number;
633
+ billingEndedAt?: number;
634
+ } | null = null;
620
635
  const uploadDeadlineBreaches: DaytonaUploadAttemptTiming[] = [];
636
+ const reportedSandboxIds = new Set<string>();
637
+ const reportedSandboxEndTimes = new Map<string, number | null>();
638
+ const runtimeResourceReportErrors = new Set<unknown>();
639
+ const reportRuntimeResource = async (acquired: {
640
+ sandbox: DaytonaSandbox;
641
+ billingStartedAt: number;
642
+ billingEndedAt?: number;
643
+ }) => {
644
+ const sandbox = acquired.sandbox;
645
+ const billingEndedAt = acquired.billingEndedAt ?? null;
646
+ if (
647
+ reportedSandboxIds.has(sandbox.id) &&
648
+ reportedSandboxEndTimes.get(sandbox.id) === billingEndedAt
649
+ ) {
650
+ return;
651
+ }
652
+ try {
653
+ await callbacks?.onRuntimeResourceAcquired?.({
654
+ kind: 'daytona_sandbox',
655
+ sandboxId: sandbox.id,
656
+ billingStartedAt: acquired.billingStartedAt,
657
+ billingEndedAt,
658
+ cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
659
+ memoryGiB: typeof sandbox.memory === 'number' ? sandbox.memory : null,
660
+ diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : null,
661
+ });
662
+ } catch (error) {
663
+ runtimeResourceReportErrors.add(error);
664
+ throw error;
665
+ }
666
+ reportedSandboxIds.add(sandbox.id);
667
+ reportedSandboxEndTimes.set(sandbox.id, billingEndedAt);
668
+ };
669
+ const reportRetiringRuntimeResource = async (acquired: {
670
+ sandbox: DaytonaSandbox;
671
+ billingStartedAt: number;
672
+ billingEndedAt?: number;
673
+ }) => {
674
+ try {
675
+ await reportRuntimeResource(acquired);
676
+ } catch (error) {
677
+ if (error instanceof RuntimeResourceFenceLostError) throw error;
678
+ console.warn('[play-runner.daytona.resource_report_deferred]', {
679
+ sandboxId: acquired.sandbox.id,
680
+ error: error instanceof Error ? error.message : String(error),
681
+ });
682
+ }
683
+ };
621
684
  let cancelExecution!: (error: Error) => void;
622
685
  const cancellationPromise = new Promise<never>((_resolve, reject) => {
623
686
  cancelExecution = reject;
624
687
  });
625
688
  const onCancel = () => {
626
689
  cancellationRequested = true;
690
+ if (activeAcquiredResource && !activeAcquiredResource.billingEndedAt) {
691
+ activeAcquiredResource.billingEndedAt = Date.now();
692
+ }
627
693
  if (cancellationCleanupStarted) {
628
694
  cancelExecution(new Error(DAYTONA_CANCELLED_ERROR));
629
695
  return;
@@ -663,28 +729,20 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
663
729
  acquiredSandboxPromise,
664
730
  cancellationPromise,
665
731
  ]);
732
+ activeAcquiredResource = acquiredSandbox;
666
733
  sandboxCleanup.activate(acquiredSandbox);
667
734
  runtimeTiming.daytonaCreateMs =
668
735
  acquiredSandbox.billingStartedAt - startedAt;
669
736
  const sandbox = acquiredSandbox.sandbox;
670
737
  sandboxForAttempt = sandbox;
671
- const resourceAcquiredNotice = callbacks?.onRuntimeResourceAcquired?.(
672
- {
673
- kind: 'daytona_sandbox',
674
- sandboxId: sandbox.id,
675
- billingStartedAt: acquiredSandbox.billingStartedAt,
676
- cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
677
- memoryGiB:
678
- typeof sandbox.memory === 'number' ? sandbox.memory : null,
679
- diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : null,
680
- },
681
- );
682
- if (resourceAcquiredNotice) {
683
- await withDaytonaOperationDeadline({
684
- operation: 'onRuntimeResourceAcquired',
685
- deadlineMs: DAYTONA_API_CALL_DEADLINE_MS,
686
- promise: resourceAcquiredNotice,
687
- });
738
+ await reportRuntimeResource(acquiredSandbox);
739
+ // Account for every acquired retry sandbox before customer code runs.
740
+ // This keeps resource persistence failures on the pre-side-effect
741
+ // side of the execution boundary and makes cancellation billing
742
+ // complete without a post-terminal write race.
743
+ await sandboxLifecycle.settlePendingCreates();
744
+ for (const acquired of sandboxLifecycle.acquiredSandboxes()) {
745
+ await reportRuntimeResource(acquired);
688
746
  }
689
747
  throwIfCancellationRequested();
690
748
 
@@ -748,6 +806,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
748
806
  elapsedMs: Date.now() - startedAt,
749
807
  });
750
808
  if (executionAttempt < DAYTONA_UPLOAD_MAX_ATTEMPTS) {
809
+ acquiredSandbox.billingEndedAt = Date.now();
810
+ await reportRetiringRuntimeResource(acquiredSandbox);
751
811
  sandboxCleanup.stashActiveSandboxForRetry();
752
812
  emitDaytonaStage(callbacks, config.context, 'recreate:start', {
753
813
  previousSandboxId: sandbox.id,
@@ -893,6 +953,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
893
953
  error: result.error,
894
954
  elapsedMs: Date.now() - startedAt,
895
955
  });
956
+ acquiredSandbox.billingEndedAt = Date.now();
957
+ await reportRetiringRuntimeResource(acquiredSandbox);
896
958
  sandboxCleanup.stashActiveSandboxForRetry();
897
959
  acquiredSandboxPromise = sandboxLifecycle.createFreshSandbox();
898
960
  continue;
@@ -964,6 +1026,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
964
1026
  elapsedMs: Date.now() - startedAt,
965
1027
  });
966
1028
  if (sandboxForAttempt) {
1029
+ activeAcquiredResource!.billingEndedAt = Date.now();
1030
+ await reportRetiringRuntimeResource(activeAcquiredResource!);
967
1031
  sandboxCleanup.stashActiveSandboxForRetry();
968
1032
  }
969
1033
  acquiredSandboxPromise = sandboxLifecycle.createFreshSandbox();
@@ -981,6 +1045,11 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
981
1045
  }),
982
1046
  );
983
1047
  } catch (error) {
1048
+ // Resource persistence belongs to the scheduler control plane. Preserve
1049
+ // its typed capacity/fence errors so Absurd can defer or fence the
1050
+ // attempt; converting them into a runner failure would terminally fail a
1051
+ // play that never began executing customer code.
1052
+ if (runtimeResourceReportErrors.has(error)) throw error;
984
1053
  emitDaytonaStage(callbacks, config.context, 'execute:error', {
985
1054
  sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
986
1055
  error: formatDaytonaError(error),
@@ -999,6 +1068,22 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
999
1068
  );
1000
1069
  } finally {
1001
1070
  callbacks?.cancellationSignal?.removeEventListener('abort', onCancel);
1071
+ if (cancellationRequested) {
1072
+ await sandboxLifecycle.settlePendingCreates();
1073
+ for (const acquired of sandboxLifecycle.acquiredSandboxes()) {
1074
+ try {
1075
+ await reportRuntimeResource(acquired);
1076
+ } catch (error) {
1077
+ console.error(
1078
+ '[play-runner.daytona.cancel_resource_report_failed]',
1079
+ {
1080
+ sandboxId: acquired.sandbox.id,
1081
+ error: error instanceof Error ? error.message : String(error),
1082
+ },
1083
+ );
1084
+ }
1085
+ }
1086
+ }
1002
1087
  }
1003
1088
  },
1004
1089
  };
@@ -14,11 +14,19 @@ export type PlayRunnerRuntimeResource =
14
14
  kind: 'daytona_sandbox';
15
15
  sandboxId: string;
16
16
  billingStartedAt: number;
17
+ billingEndedAt?: number | null;
17
18
  cpu?: number | null;
18
19
  memoryGiB?: number | null;
19
20
  diskGiB?: number | null;
20
21
  };
21
22
 
23
+ export class RuntimeResourceFenceLostError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = 'RuntimeResourceFenceLostError';
27
+ }
28
+ }
29
+
22
30
  export interface PlayRunnerCallbacks {
23
31
  onLog?: (event: PlayRunnerLogEvent) => void;
24
32
  onCheckpoint?: (checkpoint: PlayCheckpoint) => void;
@@ -84,7 +84,7 @@ export type RuntimeBillingAction =
84
84
  workflowId?: string;
85
85
  runId?: string;
86
86
  maxCreditsPerRun?: number | null;
87
- finalItem: ComputeBillingItem;
87
+ finalItem?: ComputeBillingItem;
88
88
  };
89
89
 
90
90
  export type RuntimeReceiptAction =
@@ -45,6 +45,11 @@ const pgRuntimePoolFactory: RuntimePoolFactory = (input): RuntimePool => {
45
45
  idleTimeoutMillis: input.idleTimeoutMs ?? 15_000,
46
46
  connectionTimeoutMillis: input.connectTimeoutMs ?? 10_000,
47
47
  });
48
+ pool.on('error', (error) => {
49
+ console.warn(
50
+ `[warn] runtime Postgres discarded an idle client after an error: ${error.message}`,
51
+ );
52
+ });
48
53
  return wrapPgPool(pool);
49
54
  };
50
55
 
@@ -0,0 +1,74 @@
1
+ import {
2
+ resolveDaytonaSandboxComputeItem,
3
+ type ComputeBillingItem,
4
+ } from './worker-api-types';
5
+
6
+ function finiteNonNegative(value: unknown): number | null {
7
+ return typeof value === 'number' && Number.isFinite(value)
8
+ ? Math.max(0, value)
9
+ : null;
10
+ }
11
+
12
+ /** Project executor resource facts into one idempotent item per real sandbox. */
13
+ export function resolveDaytonaRuntimeComputeItems(input: {
14
+ resources: readonly unknown[];
15
+ endedAt: number;
16
+ }): ComputeBillingItem[] {
17
+ const resourcesBySandboxId = new Map<
18
+ string,
19
+ {
20
+ startedAt: number;
21
+ endedAt: number | null;
22
+ cpu: number | null;
23
+ memoryGiB: number | null;
24
+ diskGiB: number | null;
25
+ }
26
+ >();
27
+ const items: ComputeBillingItem[] = [];
28
+ for (const resource of input.resources) {
29
+ if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
30
+ continue;
31
+ }
32
+ const value = resource as Record<string, unknown>;
33
+ const sandboxId =
34
+ typeof value.sandboxId === 'string' ? value.sandboxId.trim() : '';
35
+ if (value.kind !== 'daytona_sandbox' || !sandboxId) {
36
+ continue;
37
+ }
38
+ const billingStartedAt = finiteNonNegative(value.billingStartedAt);
39
+ if (billingStartedAt === null) continue;
40
+ const billingEndedAt = finiteNonNegative(value.billingEndedAt);
41
+ const existing = resourcesBySandboxId.get(sandboxId);
42
+ resourcesBySandboxId.set(sandboxId, {
43
+ startedAt: Math.min(
44
+ existing?.startedAt ?? billingStartedAt,
45
+ billingStartedAt,
46
+ ),
47
+ endedAt:
48
+ billingEndedAt === null
49
+ ? existing?.endedAt ?? null
50
+ : Math.max(existing?.endedAt ?? billingEndedAt, billingEndedAt),
51
+ cpu: finiteNonNegative(value.cpu) ?? existing?.cpu ?? null,
52
+ memoryGiB:
53
+ finiteNonNegative(value.memoryGiB) ?? existing?.memoryGiB ?? null,
54
+ diskGiB: finiteNonNegative(value.diskGiB) ?? existing?.diskGiB ?? null,
55
+ });
56
+ }
57
+ for (const [sandboxId, resource] of resourcesBySandboxId) {
58
+ const endedAt = resource.endedAt ?? input.endedAt;
59
+ items.push(
60
+ resolveDaytonaSandboxComputeItem({
61
+ itemId: `daytona:${sandboxId}`,
62
+ source: 'daytona',
63
+ wallTimeSeconds: Math.max(0, endedAt - resource.startedAt) / 1_000,
64
+ cpu: resource.cpu ?? 1,
65
+ memoryGiB: resource.memoryGiB ?? 1,
66
+ diskGiB: resource.diskGiB ?? 0,
67
+ sandboxId,
68
+ startedAt: resource.startedAt,
69
+ endedAt,
70
+ }),
71
+ );
72
+ }
73
+ return items;
74
+ }
@@ -1,3 +1,5 @@
1
+ import { getRuntimeEnv } from '../runtime-env';
2
+
1
3
  export const PLAY_RUNTIME_TEST_FAULT_HEADER = 'x-deepline-test-fault';
2
4
 
3
5
  export type RuntimeTestFaultName =
@@ -82,15 +84,6 @@ const RUNTIME_TEST_POLICY_WORK_BUDGET_KEYS = new Set([
82
84
  const MAX_RUNTIME_TEST_POLICY_MS = 10 * 60_000;
83
85
  const MAX_RUNTIME_TEST_POLICY_YIELD_LIMIT = 1_000_000;
84
86
 
85
- function isProductionRuntime(): boolean {
86
- const deeplineEnv = process.env.DEEPLINE_ENV?.trim().toLowerCase();
87
- if (deeplineEnv === 'production' || deeplineEnv === 'prod') return true;
88
- const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
89
- if (vercelEnv === 'production') return true;
90
- if (vercelEnv === 'preview' || vercelEnv === 'development') return false;
91
- return process.env.NODE_ENV === 'production';
92
- }
93
-
94
87
  function testRuntimeSeamsEnabled(): boolean {
95
88
  return process.env.DEEPLINE_TEST_RUNTIME_SEAMS === '1';
96
89
  }
@@ -243,7 +236,7 @@ export function validateRuntimeTestPolicyOverrides(input: {
243
236
  if (input.value === undefined || input.value === null) {
244
237
  return { ok: true, overrides: null };
245
238
  }
246
- if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
239
+ if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
247
240
  return {
248
241
  ok: false,
249
242
  status: 403,
@@ -270,7 +263,7 @@ export function readRuntimeTestFaultRegistry(input: {
270
263
  if (!headerValue) {
271
264
  return { ok: true, registry: null };
272
265
  }
273
- if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
266
+ if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
274
267
  return {
275
268
  ok: false,
276
269
  status: 403,
@@ -323,7 +316,7 @@ export function parseRuntimeTestFaultCounts(input: {
323
316
  if (!headerValue) {
324
317
  return { ok: true, counts: null, headerValue: null };
325
318
  }
326
- if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
319
+ if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
327
320
  return {
328
321
  ok: false,
329
322
  status: 403,