deepline 0.2.0 → 0.2.1

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 (31) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +13 -2
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/shared_libs/play-runtime/backend.ts +19 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +104 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +4 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +106 -3
  7. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-modal-fallback.ts +218 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +26 -7
  9. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +33 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +380 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +28 -3
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +240 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +5 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +17 -2
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +188 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +77 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +10 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +39 -0
  20. package/dist/cli/index.js +50 -26
  21. package/dist/cli/index.mjs +50 -26
  22. package/dist/index.d.mts +4 -2
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.js +50 -5
  25. package/dist/index.mjs +50 -5
  26. package/dist/plays/bundle-play-file.d.mts +2 -2
  27. package/dist/plays/bundle-play-file.d.ts +2 -2
  28. package/dist/plays/bundle-play-file.mjs +7 -1
  29. package/dist/{tool-execution-error-YDz7UMl-.d.mts → tool-execution-error-4-rhemLQ.d.mts} +7 -1
  30. package/dist/{tool-execution-error-YDz7UMl-.d.ts → tool-execution-error-4-rhemLQ.d.ts} +7 -1
  31. package/package.json +1 -1
@@ -0,0 +1,240 @@
1
+ import {
2
+ deleteDaytonaSandboxByIdWithOutcome,
3
+ readDetachedDaytonaRuntimeCompletion,
4
+ } from './backends/daytona';
5
+ import {
6
+ deleteModalSandboxById,
7
+ readDetachedModalRuntimeCompletion,
8
+ } from './backends/modal';
9
+ import type { DetachedRunnerSuspension } from '@shared_libs/play-runtime/suspension';
10
+
11
+ export type RuntimeSandboxProviderId = 'daytona' | 'modal';
12
+
13
+ export type RuntimeSandboxRef = {
14
+ schemaVersion: 1;
15
+ provider: RuntimeSandboxProviderId;
16
+ resourceId: string;
17
+ routingDomain: string | null;
18
+ };
19
+
20
+ export type RuntimeSandboxReconciliationAdapter = {
21
+ provider: RuntimeSandboxProviderId;
22
+ delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
23
+ readWorkCompletedAt(
24
+ ref: RuntimeSandboxRef,
25
+ path: string,
26
+ ): Promise<number | null>;
27
+ };
28
+
29
+ export type RuntimeSandboxReconciliation = {
30
+ delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
31
+ readWorkCompletedAt(
32
+ ref: RuntimeSandboxRef,
33
+ path: string,
34
+ ): Promise<number | null>;
35
+ };
36
+
37
+ export type RuntimeSandboxDeleteOutcome =
38
+ | {
39
+ kind: 'deleted' | 'already_absent';
40
+ routingDomain: string | null;
41
+ }
42
+ | {
43
+ kind: 'timed_out' | 'rate_limited' | 'failed';
44
+ routingDomain: string | null;
45
+ code: string;
46
+ detail: string;
47
+ };
48
+
49
+ export function runtimeSandboxDeleteSucceeded(
50
+ outcome: RuntimeSandboxDeleteOutcome,
51
+ ): outcome is Extract<
52
+ RuntimeSandboxDeleteOutcome,
53
+ { kind: 'deleted' | 'already_absent' }
54
+ > {
55
+ return outcome.kind === 'deleted' || outcome.kind === 'already_absent';
56
+ }
57
+
58
+ /** Stable identity for deduplication and task idempotency at provider seams. */
59
+ export function runtimeSandboxRefIdentityKey(ref: RuntimeSandboxRef): string {
60
+ return JSON.stringify([ref.provider, ref.resourceId, ref.routingDomain]);
61
+ }
62
+
63
+ function registry(
64
+ adapters: readonly RuntimeSandboxReconciliationAdapter[],
65
+ ): Map<RuntimeSandboxProviderId, RuntimeSandboxReconciliationAdapter> {
66
+ const result = new Map<
67
+ RuntimeSandboxProviderId,
68
+ RuntimeSandboxReconciliationAdapter
69
+ >();
70
+ for (const adapter of adapters) {
71
+ if (result.has(adapter.provider)) {
72
+ throw new Error(
73
+ `Duplicate Runtime Sandbox Adapter: ${adapter.provider}.`,
74
+ );
75
+ }
76
+ result.set(adapter.provider, adapter);
77
+ }
78
+ return result;
79
+ }
80
+
81
+ export function createRuntimeSandboxReconciliation(input: {
82
+ adapters: readonly RuntimeSandboxReconciliationAdapter[];
83
+ }): RuntimeSandboxReconciliation {
84
+ const adapters = registry(input.adapters);
85
+ const resolve = (ref: RuntimeSandboxRef) => {
86
+ const adapter = adapters.get(ref.provider);
87
+ if (!adapter) {
88
+ throw new Error(
89
+ `No Runtime Sandbox Adapter is registered for ${ref.provider}.`,
90
+ );
91
+ }
92
+ return adapter;
93
+ };
94
+ return {
95
+ delete: (ref) => resolve(ref).delete(ref),
96
+ readWorkCompletedAt: (ref, path) =>
97
+ resolve(ref).readWorkCompletedAt(ref, path),
98
+ };
99
+ }
100
+
101
+ export function runtimeSandboxRefFromResource(
102
+ value: unknown,
103
+ ): RuntimeSandboxRef | null {
104
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
105
+ const resource = value as Record<string, unknown>;
106
+ const resourceId =
107
+ typeof resource.sandboxId === 'string' ? resource.sandboxId.trim() : '';
108
+ if (!resourceId) return null;
109
+ if (resource.kind === 'daytona_sandbox') {
110
+ return {
111
+ schemaVersion: 1,
112
+ provider: 'daytona',
113
+ resourceId,
114
+ routingDomain:
115
+ typeof resource.daytonaOrganizationId === 'string' &&
116
+ resource.daytonaOrganizationId.trim()
117
+ ? resource.daytonaOrganizationId.trim()
118
+ : null,
119
+ };
120
+ }
121
+ if (resource.kind === 'modal_sandbox') {
122
+ return {
123
+ schemaVersion: 1,
124
+ provider: 'modal',
125
+ resourceId,
126
+ routingDomain:
127
+ typeof resource.modalAppId === 'string' && resource.modalAppId.trim()
128
+ ? resource.modalAppId.trim()
129
+ : null,
130
+ };
131
+ }
132
+ return null;
133
+ }
134
+
135
+ export function runtimeSandboxRefFromSuspension(
136
+ suspension: DetachedRunnerSuspension,
137
+ ): RuntimeSandboxRef {
138
+ const storedRef = (suspension as { runtimeSandboxRef?: unknown })
139
+ .runtimeSandboxRef;
140
+ if (storedRef !== undefined) {
141
+ if (
142
+ !storedRef ||
143
+ typeof storedRef !== 'object' ||
144
+ Array.isArray(storedRef)
145
+ ) {
146
+ throw new Error('Persisted Runtime Sandbox Ref is not an object.');
147
+ }
148
+ const candidate = storedRef as Record<string, unknown>;
149
+ const resourceId =
150
+ typeof candidate.resourceId === 'string'
151
+ ? candidate.resourceId.trim()
152
+ : '';
153
+ const provider = candidate.provider;
154
+ const routingDomain = candidate.routingDomain;
155
+ if (
156
+ candidate.schemaVersion !== 1 ||
157
+ (provider !== 'daytona' && provider !== 'modal') ||
158
+ !resourceId ||
159
+ resourceId !== suspension.sandboxId.trim() ||
160
+ (routingDomain !== null && typeof routingDomain !== 'string') ||
161
+ (typeof routingDomain === 'string' && !routingDomain.trim()) ||
162
+ (suspension.sandboxProvider !== undefined &&
163
+ suspension.sandboxProvider !== provider)
164
+ ) {
165
+ throw new Error(
166
+ `Persisted Runtime Sandbox Ref is invalid or inconsistent for sandbox ${suspension.sandboxId}.`,
167
+ );
168
+ }
169
+ return {
170
+ schemaVersion: 1,
171
+ provider,
172
+ resourceId,
173
+ routingDomain:
174
+ typeof routingDomain === 'string' ? routingDomain.trim() : null,
175
+ };
176
+ }
177
+ if (
178
+ suspension.sandboxProvider !== undefined &&
179
+ suspension.sandboxProvider !== 'daytona' &&
180
+ suspension.sandboxProvider !== 'modal'
181
+ ) {
182
+ throw new Error(
183
+ `Legacy detached runner has unsupported sandbox provider ${String(suspension.sandboxProvider)}.`,
184
+ );
185
+ }
186
+ return {
187
+ schemaVersion: 1,
188
+ provider: suspension.sandboxProvider === 'modal' ? 'modal' : 'daytona',
189
+ resourceId: suspension.sandboxId,
190
+ // Legacy suspensions never persisted ownership. Cleanup still uses the
191
+ // provider-scoped credential; resource-backed cleanup carries the domain.
192
+ routingDomain: null,
193
+ };
194
+ }
195
+
196
+ export const defaultRuntimeSandboxReconciliation =
197
+ createRuntimeSandboxReconciliation({
198
+ adapters: [
199
+ {
200
+ provider: 'daytona',
201
+ delete: async (ref) => {
202
+ const outcome = await deleteDaytonaSandboxByIdWithOutcome({
203
+ sandboxId: ref.resourceId,
204
+ timeoutSeconds: 30,
205
+ expectedOrganizationId: ref.routingDomain,
206
+ allowUnscopedAlreadyAbsent: ref.routingDomain === null,
207
+ });
208
+ return {
209
+ ...outcome,
210
+ routingDomain: outcome.organizationId,
211
+ };
212
+ },
213
+ readWorkCompletedAt: (ref, path) =>
214
+ readDetachedDaytonaRuntimeCompletion({
215
+ sandboxId: ref.resourceId,
216
+ runtimeCompletedPath: path,
217
+ expectedOrganizationId: ref.routingDomain,
218
+ }),
219
+ },
220
+ {
221
+ provider: 'modal',
222
+ delete: async (ref) => {
223
+ const outcome = await deleteModalSandboxById({
224
+ sandboxId: ref.resourceId,
225
+ expectedAppId: ref.routingDomain,
226
+ });
227
+ return {
228
+ ...outcome,
229
+ routingDomain: outcome.appId,
230
+ };
231
+ },
232
+ readWorkCompletedAt: (ref, path) =>
233
+ readDetachedModalRuntimeCompletion({
234
+ sandboxId: ref.resourceId,
235
+ runtimeCompletedPath: path,
236
+ expectedAppId: ref.routingDomain,
237
+ }),
238
+ },
239
+ ],
240
+ });
@@ -10,11 +10,15 @@ import type {
10
10
  } from '@shared_libs/play-runtime/protocol';
11
11
 
12
12
  export type PlayRunnerRuntimeResource = {
13
- kind: 'daytona_sandbox';
13
+ kind: 'daytona_sandbox' | 'modal_sandbox';
14
14
  sandboxId: string;
15
+ /** Runtime estate that owns provider credentials and durable cleanup. */
16
+ runtimeEnvironment?: 'preview' | 'production';
15
17
  daytonaEnvironment?: 'preview' | 'production';
16
18
  /** Stable, non-secret provider ownership domain returned by Daytona. */
17
19
  daytonaOrganizationId?: string;
20
+ /** Stable, non-secret Modal ownership domain returned at acquisition. */
21
+ modalAppId?: string;
18
22
  billingStartedAt: number;
19
23
  billingEndedAt?: number | null;
20
24
  /** Customer liability ceiling for this physical sandbox. */
@@ -1,3 +1,5 @@
1
+ import { PLAY_RUNTIME_BACKENDS, type PlayRuntimeBackendId } from './backend';
2
+
1
3
  export const PLAY_RUNTIME_ENVIRONMENTS = ['preview'] as const;
2
4
 
3
5
  export type PlayRuntimeEnvironment = (typeof PLAY_RUNTIME_ENVIRONMENTS)[number];
@@ -6,6 +8,8 @@ export type PlayRuntimeSelection = {
6
8
  environment: 'preview';
7
9
  /** Caller-named isolation scope inside a remote runtime environment. */
8
10
  namespace: string;
11
+ /** Explicit managed-sandbox executor. Omission preserves Daytona compatibility. */
12
+ backend?: Extract<PlayRuntimeBackendId, 'daytona' | 'modal'>;
9
13
  };
10
14
 
11
15
  const PLAY_RUNTIME_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]{0,30}$/;
@@ -24,14 +28,25 @@ export function normalizePlayRuntimeSelection(
24
28
  const record = value as Record<string, unknown>;
25
29
  if (
26
30
  Object.keys(record).some(
27
- (key) => key !== 'environment' && key !== 'namespace',
31
+ (key) =>
32
+ key !== 'environment' && key !== 'namespace' && key !== 'backend',
28
33
  ) ||
29
34
  record.environment !== 'preview'
30
35
  ) {
31
36
  return null;
32
37
  }
33
38
  const namespace = normalizePlayRuntimeNamespace(record.namespace);
34
- return namespace ? { environment: 'preview', namespace } : null;
39
+ if (!namespace) return null;
40
+ if (record.backend === undefined) {
41
+ return { environment: 'preview', namespace };
42
+ }
43
+ if (
44
+ record.backend !== PLAY_RUNTIME_BACKENDS.daytona &&
45
+ record.backend !== PLAY_RUNTIME_BACKENDS.modal
46
+ ) {
47
+ return null;
48
+ }
49
+ return { environment: 'preview', namespace, backend: record.backend };
35
50
  }
36
51
 
37
52
  export function normalizePlayRuntimeEnvironment(
@@ -0,0 +1,188 @@
1
+ import { PLAY_RUNTIME_BACKENDS, type PlayRuntimeBackendId } from './backend';
2
+
3
+ export const RUNTIME_SANDBOX_PROVIDERS = {
4
+ daytona: 'daytona',
5
+ modal: 'modal',
6
+ } as const;
7
+
8
+ export type RuntimeSandboxProvider =
9
+ (typeof RUNTIME_SANDBOX_PROVIDERS)[keyof typeof RUNTIME_SANDBOX_PROVIDERS];
10
+
11
+ /**
12
+ * Stable durable ids. Changing provider order or transition rules creates a
13
+ * new version instead of changing the meaning of an already-queued launch.
14
+ */
15
+ export const RUNTIME_SANDBOX_PLACEMENT_POLICIES = {
16
+ daytonaOnlyV1: 'daytona_only@1',
17
+ daytonaThenModalV1: 'daytona_then_modal@1',
18
+ modalOnlyV1: 'modal_only@1',
19
+ } as const;
20
+
21
+ export type RuntimeSandboxPlacementPolicyId =
22
+ (typeof RUNTIME_SANDBOX_PLACEMENT_POLICIES)[keyof typeof RUNTIME_SANDBOX_PLACEMENT_POLICIES];
23
+
24
+ export type RuntimeSandboxPlacementFailureReason =
25
+ | 'provider_capacity_exhausted'
26
+ | 'provider_start_timeout_before_execution';
27
+
28
+ export type RuntimeSandboxPlacementTransition = Readonly<{
29
+ from: RuntimeSandboxProvider;
30
+ to: RuntimeSandboxProvider;
31
+ stage: 'acquisition';
32
+ reasons: readonly RuntimeSandboxPlacementFailureReason[];
33
+ }>;
34
+
35
+ export type RuntimeSandboxPlacementPolicy = Readonly<{
36
+ id: RuntimeSandboxPlacementPolicyId;
37
+ compatibilityRuntimeBackend:
38
+ | typeof PLAY_RUNTIME_BACKENDS.daytona
39
+ | typeof PLAY_RUNTIME_BACKENDS.modal;
40
+ providers: readonly [RuntimeSandboxProvider, ...RuntimeSandboxProvider[]];
41
+ transitions: readonly RuntimeSandboxPlacementTransition[];
42
+ requiredCredentialEnv: readonly string[];
43
+ cleanupProviders: readonly RuntimeSandboxProvider[];
44
+ }>;
45
+
46
+ const DAYTONA_THEN_MODAL_V1: RuntimeSandboxPlacementPolicy = {
47
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaThenModalV1,
48
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.daytona,
49
+ providers: [
50
+ RUNTIME_SANDBOX_PROVIDERS.daytona,
51
+ RUNTIME_SANDBOX_PROVIDERS.modal,
52
+ ],
53
+ transitions: [
54
+ {
55
+ from: RUNTIME_SANDBOX_PROVIDERS.daytona,
56
+ to: RUNTIME_SANDBOX_PROVIDERS.modal,
57
+ stage: 'acquisition',
58
+ reasons: [
59
+ 'provider_capacity_exhausted',
60
+ 'provider_start_timeout_before_execution',
61
+ ],
62
+ },
63
+ ],
64
+ requiredCredentialEnv: [
65
+ 'DAYTONA_API_KEY',
66
+ 'MODAL_TOKEN_ID',
67
+ 'MODAL_TOKEN_SECRET',
68
+ ],
69
+ cleanupProviders: [
70
+ RUNTIME_SANDBOX_PROVIDERS.daytona,
71
+ RUNTIME_SANDBOX_PROVIDERS.modal,
72
+ ],
73
+ };
74
+
75
+ /**
76
+ * Frozen compatibility policy for launches written before placement policy ids
77
+ * existed. It is retained until every pre-policy release lane has drained.
78
+ */
79
+ const DAYTONA_ONLY_V1: RuntimeSandboxPlacementPolicy = {
80
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaOnlyV1,
81
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.daytona,
82
+ providers: [RUNTIME_SANDBOX_PROVIDERS.daytona],
83
+ transitions: [],
84
+ requiredCredentialEnv: ['DAYTONA_API_KEY'],
85
+ cleanupProviders: [RUNTIME_SANDBOX_PROVIDERS.daytona],
86
+ };
87
+
88
+ const MODAL_ONLY_V1: RuntimeSandboxPlacementPolicy = {
89
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.modalOnlyV1,
90
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.modal,
91
+ providers: [RUNTIME_SANDBOX_PROVIDERS.modal],
92
+ transitions: [],
93
+ requiredCredentialEnv: ['MODAL_TOKEN_ID', 'MODAL_TOKEN_SECRET'],
94
+ cleanupProviders: [RUNTIME_SANDBOX_PROVIDERS.modal],
95
+ };
96
+
97
+ export const RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG: Readonly<
98
+ Record<RuntimeSandboxPlacementPolicyId, RuntimeSandboxPlacementPolicy>
99
+ > = {
100
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaOnlyV1]: DAYTONA_ONLY_V1,
101
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaThenModalV1]:
102
+ DAYTONA_THEN_MODAL_V1,
103
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.modalOnlyV1]: MODAL_ONLY_V1,
104
+ };
105
+
106
+ export function resolveRuntimeSandboxPlacementPolicy(
107
+ id: RuntimeSandboxPlacementPolicyId | string,
108
+ ): RuntimeSandboxPlacementPolicy {
109
+ const normalized = id.trim();
110
+ const policy =
111
+ RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG[
112
+ normalized as RuntimeSandboxPlacementPolicyId
113
+ ];
114
+ if (!policy) {
115
+ throw new Error(
116
+ `Unsupported runtime sandbox placement policy "${normalized}". Expected one of: ${Object.keys(
117
+ RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG,
118
+ ).join(', ')}.`,
119
+ );
120
+ }
121
+ return policy;
122
+ }
123
+
124
+ /**
125
+ * Compatibility Adapter for the public runtimeBackend selector. The returned
126
+ * policy id, not this selector, is the durable execution contract.
127
+ */
128
+ export function runtimeSandboxPlacementPolicyForBackend(
129
+ backend: PlayRuntimeBackendId,
130
+ ): RuntimeSandboxPlacementPolicy | null {
131
+ if (backend === PLAY_RUNTIME_BACKENDS.daytona) {
132
+ return DAYTONA_THEN_MODAL_V1;
133
+ }
134
+ if (backend === PLAY_RUNTIME_BACKENDS.modal) {
135
+ return MODAL_ONLY_V1;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ export function runtimeSandboxPlacementPoliciesForBackend(
141
+ backend: PlayRuntimeBackendId,
142
+ ): readonly RuntimeSandboxPlacementPolicy[] {
143
+ if (backend === PLAY_RUNTIME_BACKENDS.localProcess) return [];
144
+ return Object.values(RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG).filter(
145
+ (policy) => policy.compatibilityRuntimeBackend === backend,
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Old queued launches have no policy id. Derive their historical meaning from
151
+ * runtimeBackend only when the durable field is absent. Unknown stored ids fail
152
+ * loudly rather than silently changing execution semantics.
153
+ */
154
+ export function resolveLaunchRuntimeSandboxPlacementPolicy(input: {
155
+ runtimeSandboxPlacementPolicyId?: string | null;
156
+ runtimeBackend: PlayRuntimeBackendId;
157
+ }): RuntimeSandboxPlacementPolicy | null {
158
+ const storedId = input.runtimeSandboxPlacementPolicyId?.trim();
159
+ if (storedId) {
160
+ const policy = resolveRuntimeSandboxPlacementPolicy(storedId);
161
+ if (policy.compatibilityRuntimeBackend !== input.runtimeBackend) {
162
+ throw new Error(
163
+ `Runtime sandbox placement policy ${policy.id} is incompatible with runtime backend ${input.runtimeBackend}.`,
164
+ );
165
+ }
166
+ return policy;
167
+ }
168
+ if (input.runtimeBackend === PLAY_RUNTIME_BACKENDS.daytona) {
169
+ return DAYTONA_ONLY_V1;
170
+ }
171
+ return runtimeSandboxPlacementPolicyForBackend(input.runtimeBackend);
172
+ }
173
+
174
+ export function canTransitionRuntimeSandboxPlacement(input: {
175
+ policy: RuntimeSandboxPlacementPolicy;
176
+ from: RuntimeSandboxProvider;
177
+ to: RuntimeSandboxProvider;
178
+ stage: 'acquisition';
179
+ reason: RuntimeSandboxPlacementFailureReason;
180
+ }): boolean {
181
+ return input.policy.transitions.some(
182
+ (transition) =>
183
+ transition.from === input.from &&
184
+ transition.to === input.to &&
185
+ transition.stage === input.stage &&
186
+ transition.reasons.includes(input.reason),
187
+ );
188
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  resolveDaytonaSandboxComputeItem,
3
+ resolveModalSandboxComputeItem,
3
4
  type ComputeBillingItem,
4
5
  } from './worker-api-types';
5
6
 
@@ -9,6 +10,82 @@ function finiteNonNegative(value: unknown): number | null {
9
10
  : null;
10
11
  }
11
12
 
13
+ /** Project every provider-owned sandbox into a distinct internal compute item. */
14
+ export function resolveRuntimeSandboxComputeItems(input: {
15
+ resources: readonly unknown[];
16
+ endedAt: number;
17
+ }): ComputeBillingItem[] {
18
+ const daytonaResources: unknown[] = [];
19
+ const modalResources = new Map<
20
+ string,
21
+ {
22
+ startedAt: number;
23
+ endedAt: number | null;
24
+ physicalCpuCores: number | null;
25
+ memoryGiB: number | null;
26
+ maxBillingDurationSeconds: number | null;
27
+ }
28
+ >();
29
+ for (const resource of input.resources) {
30
+ if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
31
+ continue;
32
+ }
33
+ const value = resource as Record<string, unknown>;
34
+ if (value.kind === 'daytona_sandbox') {
35
+ daytonaResources.push(resource);
36
+ continue;
37
+ }
38
+ if (value.kind !== 'modal_sandbox') continue;
39
+ const sandboxId =
40
+ typeof value.sandboxId === 'string' ? value.sandboxId.trim() : '';
41
+ const startedAt = finiteNonNegative(value.billingStartedAt);
42
+ if (!sandboxId || startedAt === null) continue;
43
+ const endedAt = finiteNonNegative(value.billingEndedAt);
44
+ const existing = modalResources.get(sandboxId);
45
+ modalResources.set(sandboxId, {
46
+ startedAt: Math.min(existing?.startedAt ?? startedAt, startedAt),
47
+ endedAt:
48
+ endedAt === null
49
+ ? (existing?.endedAt ?? null)
50
+ : Math.max(existing?.endedAt ?? endedAt, endedAt),
51
+ physicalCpuCores:
52
+ finiteNonNegative(value.cpu) ?? existing?.physicalCpuCores ?? null,
53
+ memoryGiB:
54
+ finiteNonNegative(value.memoryGiB) ?? existing?.memoryGiB ?? null,
55
+ maxBillingDurationSeconds:
56
+ finiteNonNegative(value.maxBillingDurationSeconds) ??
57
+ existing?.maxBillingDurationSeconds ??
58
+ null,
59
+ });
60
+ }
61
+ const items = resolveDaytonaRuntimeComputeItems({
62
+ resources: daytonaResources,
63
+ endedAt: input.endedAt,
64
+ });
65
+ for (const [sandboxId, resource] of modalResources) {
66
+ const observedEndedAt = resource.endedAt ?? input.endedAt;
67
+ const endedAt =
68
+ resource.maxBillingDurationSeconds === null
69
+ ? observedEndedAt
70
+ : Math.min(
71
+ observedEndedAt,
72
+ resource.startedAt + resource.maxBillingDurationSeconds * 1_000,
73
+ );
74
+ items.push(
75
+ resolveModalSandboxComputeItem({
76
+ itemId: `modal:${sandboxId}`,
77
+ wallTimeSeconds: Math.max(0, endedAt - resource.startedAt) / 1_000,
78
+ physicalCpuCores: resource.physicalCpuCores ?? 0.5,
79
+ memoryGiB: resource.memoryGiB ?? 1,
80
+ sandboxId,
81
+ startedAt: resource.startedAt,
82
+ endedAt,
83
+ }),
84
+ );
85
+ }
86
+ return items;
87
+ }
88
+
12
89
  /** Project executor resource facts into one idempotent item per real sandbox. */
13
90
  export function resolveDaytonaRuntimeComputeItems(input: {
14
91
  resources: readonly unknown[];
@@ -21,6 +21,7 @@ import type { RuntimeAuthorityDescriptor } from './execution-capabilities';
21
21
  import type { PlayRunnerRuntimeTiming } from './protocol';
22
22
  import type { RuntimeTestPolicyOverrides } from './test-runtime-seams';
23
23
  import type { PlayRunInputPayload } from './play-input';
24
+ import type { RuntimeSandboxPlacementPolicyId } from './runtime-sandbox-placement-policy';
24
25
 
25
26
  export const PLAY_SCHEDULER_BACKENDS = {
26
27
  /**
@@ -114,6 +115,12 @@ export type PlaySchedulerSubmitInput = {
114
115
  executionProfile?: string | null;
115
116
  /** runner backend to use for executing attempts */
116
117
  runtimeBackend: string;
118
+ /**
119
+ * Versioned managed-sandbox placement contract. Optional only so launches
120
+ * queued before the policy field existed can drain using runtimeBackend's
121
+ * historical meaning.
122
+ */
123
+ runtimeSandboxPlacementPolicyId?: RuntimeSandboxPlacementPolicyId | null;
117
124
  /** dedup backend for cross-attempt cross-process idempotency */
118
125
  dedupBackend: string;
119
126
  /** If known at submit time, total input rows (for partition decisions). */
@@ -32,6 +32,16 @@ export type PlayExecutionSuspension =
32
32
  * the key under which its pushed terminal is recorded and its wake event
33
33
  * is named. */
34
34
  runnerAttempt: number;
35
+ /** Missing on historical records, which are Daytona. */
36
+ sandboxProvider?: 'daytona' | 'modal';
37
+ /** Versioned physical identity for provider-safe wake and cleanup.
38
+ * Legacy fields remain until every launch-pinned release has drained. */
39
+ runtimeSandboxRef?: {
40
+ schemaVersion: 1;
41
+ provider: 'daytona' | 'modal';
42
+ resourceId: string;
43
+ routingDomain: string | null;
44
+ };
35
45
  sandboxId: string;
36
46
  sessionId: string;
37
47
  cmdId: string;
@@ -42,6 +42,12 @@ export const DAYTONA_COMPUTE_PRICING_USD = {
42
42
  includedStorageGiB: 5,
43
43
  } as const;
44
44
 
45
+ /** Modal Sandbox on-demand rates, per second (modal.com/pricing, 2026-07-29). */
46
+ export const MODAL_SANDBOX_COMPUTE_PRICING_USD = {
47
+ physicalCoreSecond: 0.00003942,
48
+ memoryGiBSecond: 0.00000667,
49
+ } as const;
50
+
45
51
  const NEON_COMPUTE_PRICING_USD = {
46
52
  cuHour: 0.106,
47
53
  defaultCu: 1,
@@ -94,6 +100,39 @@ export function resolveDaytonaSandboxComputeItem(input: {
94
100
  };
95
101
  }
96
102
 
103
+ export function resolveModalSandboxComputeItem(input: {
104
+ itemId: string;
105
+ wallTimeSeconds: number;
106
+ physicalCpuCores: number;
107
+ memoryGiB: number;
108
+ sandboxId?: string | null;
109
+ startedAt?: number;
110
+ endedAt?: number;
111
+ }): ComputeBillingItem {
112
+ const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
113
+ const physicalCpuCores = Math.max(0, input.physicalCpuCores);
114
+ const memoryGiB = Math.max(0, input.memoryGiB);
115
+ const providerCostUsd = roundUsd(
116
+ billableSeconds *
117
+ (physicalCpuCores * MODAL_SANDBOX_COMPUTE_PRICING_USD.physicalCoreSecond +
118
+ memoryGiB * MODAL_SANDBOX_COMPUTE_PRICING_USD.memoryGiBSecond),
119
+ );
120
+ return {
121
+ itemId: input.itemId,
122
+ source: 'modal',
123
+ unit: 'sandbox_second',
124
+ units: billableSeconds,
125
+ providerCostUsd,
126
+ metadata: {
127
+ sandboxId: input.sandboxId ?? null,
128
+ physicalCpuCores,
129
+ memoryGiB,
130
+ startedAt: input.startedAt,
131
+ endedAt: input.endedAt,
132
+ },
133
+ };
134
+ }
135
+
97
136
  export function resolveNeonComputeItem(input: {
98
137
  itemId: string;
99
138
  activeSeconds: number;