deepline 0.3.17 → 0.3.18
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.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +32 -18
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +254 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +5 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +344 -26
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-reliability-policy.ts +391 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-traffic-policy.ts +125 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +36 -39
- package/dist/cli/index.js +252 -1
- package/dist/cli/index.mjs +252 -1
- package/dist/index.js +252 -1
- package/dist/index.mjs +252 -1
- package/dist/install-integrity.json +4 -0
- package/package.json +1 -1
|
@@ -192,7 +192,7 @@ export const SDK_RELEASE = {
|
|
|
192
192
|
// 0.3.0 introduces raw-v2: complete scrubbed provider responses are
|
|
193
193
|
// available at toolResponse.rawV2 while toolResponse.raw and all declared
|
|
194
194
|
// getters keep their established compatibility behavior.
|
|
195
|
-
version: '0.3.
|
|
195
|
+
version: '0.3.18',
|
|
196
196
|
updateSummary:
|
|
197
197
|
'New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.',
|
|
198
198
|
contracts: {
|
|
@@ -52,6 +52,11 @@ import {
|
|
|
52
52
|
import { vercelProtectionBypassHeaders } from '@shared_libs/play-runtime/vercel-protection';
|
|
53
53
|
import type { RuntimeReceiptAction } from '@shared_libs/play-runtime/runtime-actions';
|
|
54
54
|
import { RUNTIME_CAPACITY_POLICY } from '@shared_libs/play-runtime/runtime-capacity-policy';
|
|
55
|
+
import {
|
|
56
|
+
DEFAULT_RUNTIME_TRAFFIC_POLICY,
|
|
57
|
+
isRuntimeTrafficPolicy,
|
|
58
|
+
type RuntimeTrafficPolicy,
|
|
59
|
+
} from '@shared_libs/play-runtime/runtime-traffic-policy';
|
|
55
60
|
|
|
56
61
|
export type StoredPlayArtifactPayload = {
|
|
57
62
|
sourceCode: string;
|
|
@@ -217,6 +222,7 @@ type RuntimeApiRequest =
|
|
|
217
222
|
| ({
|
|
218
223
|
action: 'governor_budget_charge';
|
|
219
224
|
} & PlayRunnerBudgetChargeInput)
|
|
225
|
+
| { action: 'get_runtime_traffic_policy' }
|
|
220
226
|
| RuntimeReceiptAction;
|
|
221
227
|
|
|
222
228
|
export type WorkerRuntimeApiContext = {
|
|
@@ -1472,6 +1478,45 @@ export async function chargeGovernorBudgetViaAppRuntime(
|
|
|
1472
1478
|
});
|
|
1473
1479
|
}
|
|
1474
1480
|
|
|
1481
|
+
export async function getRuntimeTrafficPolicyViaAppRuntime(
|
|
1482
|
+
context: WorkerRuntimeApiContext,
|
|
1483
|
+
): Promise<RuntimeTrafficPolicy> {
|
|
1484
|
+
const response = await postAppRuntimeApi<unknown>(context, {
|
|
1485
|
+
action: 'get_runtime_traffic_policy',
|
|
1486
|
+
});
|
|
1487
|
+
if (isRuntimeTrafficPolicy(response)) return response;
|
|
1488
|
+
// A 200 response is not enough to activate an incident control. A rollout
|
|
1489
|
+
// mismatch or proxy body must behave as a control-plane outage (and use the
|
|
1490
|
+
// caller's visible compiled-default fallback), never as an implicit limit.
|
|
1491
|
+
throw new AppRuntimeApiResponseError({
|
|
1492
|
+
action: 'get_runtime_traffic_policy',
|
|
1493
|
+
status: 502,
|
|
1494
|
+
code: 'runtime_traffic_policy_invalid_response',
|
|
1495
|
+
retryable: true,
|
|
1496
|
+
detail:
|
|
1497
|
+
'Runtime traffic policy response did not match the versioned contract.',
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* The incident policy is a load-reduction overlay, never an execution
|
|
1503
|
+
* prerequisite. During an app/Convex rolling deploy or a control-plane outage
|
|
1504
|
+
* it is safer to use the compiled normal policy than to strand every new Play
|
|
1505
|
+
* before sandbox creation. Callers must emit the returned diagnostic; this is
|
|
1506
|
+
* intentionally a visible fail-open, not a silent fallback.
|
|
1507
|
+
*/
|
|
1508
|
+
export function fallbackRuntimeTrafficPolicyForUnavailableControlPlane(
|
|
1509
|
+
error: unknown,
|
|
1510
|
+
): RuntimeTrafficPolicy | null {
|
|
1511
|
+
if (
|
|
1512
|
+
error instanceof AppRuntimeApiTransportError ||
|
|
1513
|
+
(error instanceof AppRuntimeApiResponseError && error.status >= 500)
|
|
1514
|
+
) {
|
|
1515
|
+
return DEFAULT_RUNTIME_TRAFFIC_POLICY;
|
|
1516
|
+
}
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1475
1520
|
export async function writeStagedFileFromAppRuntime(
|
|
1476
1521
|
context: WorkerRuntimeApiContext,
|
|
1477
1522
|
file: Pick<PlayStagedFileRef, 'storageKey'>,
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
} from './run-execution-scope';
|
|
77
77
|
import { DEFAULT_RUNTIME_EXECUTION_CAPABILITIES } from './execution-capabilities';
|
|
78
78
|
import { vercelProtectionBypassHeader } from './vercel-protection';
|
|
79
|
+
import { RUNTIME_RELIABILITY_POLICY } from './runtime-reliability-policy';
|
|
79
80
|
export {
|
|
80
81
|
RuntimeSheetRowsBlockedError,
|
|
81
82
|
type RuntimeSheetBlockedRowDetail,
|
|
@@ -418,11 +419,15 @@ const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
|
418
419
|
// a bounded, repairable transport failure that row-isolation settles per row
|
|
419
420
|
// instead of hanging the whole run.
|
|
420
421
|
const DEFAULT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000 + 30_000;
|
|
421
|
-
const FETCH_TRANSPORT_MAX_ATTEMPTS =
|
|
422
|
+
const FETCH_TRANSPORT_MAX_ATTEMPTS =
|
|
423
|
+
RUNTIME_RELIABILITY_POLICY.egress.fetchMaxAttempts;
|
|
422
424
|
const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
|
|
423
|
-
const CTX_FETCH_HEADERS_TIMEOUT_MS =
|
|
424
|
-
|
|
425
|
-
const
|
|
425
|
+
const CTX_FETCH_HEADERS_TIMEOUT_MS =
|
|
426
|
+
RUNTIME_RELIABILITY_POLICY.egress.fetchHeadersTimeoutMs;
|
|
427
|
+
const CTX_FETCH_BODY_TIMEOUT_MS =
|
|
428
|
+
RUNTIME_RELIABILITY_POLICY.egress.fetchBodyTimeoutMs;
|
|
429
|
+
const CTX_FETCH_TOTAL_TIMEOUT_MS =
|
|
430
|
+
RUNTIME_RELIABILITY_POLICY.egress.fetchTotalTimeoutMs;
|
|
426
431
|
// cloudflared returns this branded HTML when its connection to the local app
|
|
427
432
|
// resets. It is a transport failure before the app can reach a provider, not a
|
|
428
433
|
// provider 502; require both the branded page and our development tunnel host.
|
|
@@ -430,6 +435,12 @@ const DEEPLINE_DEVELOPER_TUNNEL_ORIGIN_502_PATTERN =
|
|
|
430
435
|
/\bdeeplinedeveloper\.com\s*\|\s*502\s*:\s*bad gateway\b/i;
|
|
431
436
|
const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
|
|
432
437
|
const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
|
|
438
|
+
// A newly claimed receipt should normally use its scheduled heartbeat. When a
|
|
439
|
+
// test/SEV policy deliberately gives it only a moment of lease life, however,
|
|
440
|
+
// scheduler setup and the first provider-dispatch turn can consume that entire
|
|
441
|
+
// window before the timer fires. Renew once before fan-out in that narrow case.
|
|
442
|
+
// Production's normal multi-minute lease remains on the zero-extra-query path.
|
|
443
|
+
const IMMEDIATE_PENDING_RECEIPT_HEARTBEAT_THRESHOLD_MS = 100;
|
|
433
444
|
// Per-row sanity cap on distinct inline child invocations. `maxPlayCallDepth`
|
|
434
445
|
// (governor) already bounds nesting; this bounds fan-WIDTH from one active row
|
|
435
446
|
// resolver. The dedupe set is row-local, never retained for the full run.
|
|
@@ -10354,6 +10365,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10354
10365
|
// explicit backend capability; retain the immediate ownership check
|
|
10355
10366
|
// for those receipts.
|
|
10356
10367
|
if (claimsEstablishedExecutionFence) {
|
|
10368
|
+
if (
|
|
10369
|
+
ownershipConfirmedUntilMs - Date.now() <=
|
|
10370
|
+
IMMEDIATE_PENDING_RECEIPT_HEARTBEAT_THRESHOLD_MS
|
|
10371
|
+
) {
|
|
10372
|
+
const outcome = await heartbeatOwnedRequests();
|
|
10373
|
+
if (outcome === 'active') supervisor.start();
|
|
10374
|
+
return;
|
|
10375
|
+
}
|
|
10357
10376
|
supervisor.start();
|
|
10358
10377
|
return;
|
|
10359
10378
|
}
|
|
@@ -12183,26 +12202,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12183
12202
|
if (heartbeatFailure) {
|
|
12184
12203
|
throw heartbeatFailure;
|
|
12185
12204
|
}
|
|
12186
|
-
if (error instanceof ToolExecuteInvalidJsonError) {
|
|
12187
|
-
throw createToolHttpError(
|
|
12188
|
-
toolErrorSchemaVersion,
|
|
12189
|
-
`Tool ${toolId} returned an invalid JSON response body.`,
|
|
12190
|
-
null,
|
|
12191
|
-
response?.status ?? 0,
|
|
12192
|
-
'repairable',
|
|
12193
|
-
);
|
|
12194
|
-
}
|
|
12195
12205
|
const transportError = abortController?.signal.aborted
|
|
12196
12206
|
? abortController.signal.reason instanceof Error
|
|
12197
12207
|
? abortController.signal.reason
|
|
12198
12208
|
: new Error(
|
|
12199
12209
|
`Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
|
|
12200
12210
|
)
|
|
12201
|
-
: error instanceof ToolExecuteResponseBodyTransportError
|
|
12211
|
+
: error instanceof ToolExecuteResponseBodyTransportError ||
|
|
12212
|
+
error instanceof ToolExecuteInvalidJsonError
|
|
12202
12213
|
? error.cause
|
|
12203
12214
|
: error;
|
|
12204
12215
|
if (
|
|
12205
|
-
error instanceof ToolExecuteResponseBodyTransportError
|
|
12216
|
+
(error instanceof ToolExecuteResponseBodyTransportError ||
|
|
12217
|
+
error instanceof ToolExecuteInvalidJsonError) &&
|
|
12206
12218
|
response?.status === 402
|
|
12207
12219
|
) {
|
|
12208
12220
|
const diagnostic = describeTransportError(transportError);
|
|
@@ -12221,7 +12233,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12221
12233
|
const ambiguousDispatchedFailure =
|
|
12222
12234
|
fetchDispatched &&
|
|
12223
12235
|
(response === null ||
|
|
12224
|
-
error instanceof ToolExecuteResponseBodyTransportError
|
|
12236
|
+
error instanceof ToolExecuteResponseBodyTransportError ||
|
|
12237
|
+
error instanceof ToolExecuteInvalidJsonError);
|
|
12225
12238
|
const hasDurableInvocationIdentity = Boolean(
|
|
12226
12239
|
durableCallReceiptKey &&
|
|
12227
12240
|
providerIdempotencyKey &&
|
|
@@ -12277,8 +12290,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12277
12290
|
})}`,
|
|
12278
12291
|
);
|
|
12279
12292
|
const failureBoundary =
|
|
12280
|
-
error instanceof ToolExecuteResponseBodyTransportError
|
|
12281
|
-
|
|
12293
|
+
error instanceof ToolExecuteResponseBodyTransportError ||
|
|
12294
|
+
error instanceof ToolExecuteInvalidJsonError
|
|
12295
|
+
? 'response body could not be verified after response headers'
|
|
12282
12296
|
: 'request transport failed after dispatch before response headers';
|
|
12283
12297
|
throw createToolHttpError(
|
|
12284
12298
|
toolErrorSchemaVersion,
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { RUNTIME_RELIABILITY_POLICY } from './runtime-reliability-policy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed recovery facts that cross the provider-backend seam.
|
|
5
|
+
*
|
|
6
|
+
* A runner backend is allowed to say only that customer code has not begun
|
|
7
|
+
* and name the provider-side condition it observed. The scheduler owns the
|
|
8
|
+
* durable attempt/defer counters, deadline, and circuit state; keeping those
|
|
9
|
+
* controls out of provider adapters prevents every adapter from inventing a
|
|
10
|
+
* subtly different retry loop.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const PLAY_RUN_RECOVERY_ERROR_NAME = 'PlayRunRecoveryError';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hard platform defaults for a dependency-wide pre-code outage. They are
|
|
17
|
+
* deliberately finite; scheduler persistence records every defer so an
|
|
18
|
+
* operator can inspect or redrive exhausted work instead of inheriting an
|
|
19
|
+
* invisible infinite loop. A later control-plane policy may narrow these
|
|
20
|
+
* values, never exceed these ceilings.
|
|
21
|
+
*/
|
|
22
|
+
export const PLAY_RUN_RECOVERY_DEFER_POLICY = {
|
|
23
|
+
maxDefers: RUNTIME_RELIABILITY_POLICY.recovery.maxDefers,
|
|
24
|
+
horizonMs: RUNTIME_RELIABILITY_POLICY.recovery.horizonMs,
|
|
25
|
+
deferBaseSeconds: RUNTIME_RELIABILITY_POLICY.recovery.deferBaseSeconds,
|
|
26
|
+
deferMaxSeconds: RUNTIME_RELIABILITY_POLICY.recovery.deferMaxSeconds,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function recoveryDeferDelaySeconds(deferCount: number): number {
|
|
30
|
+
const index = Math.max(0, Math.floor(deferCount) - 1);
|
|
31
|
+
return Math.min(
|
|
32
|
+
PLAY_RUN_RECOVERY_DEFER_POLICY.deferMaxSeconds,
|
|
33
|
+
PLAY_RUN_RECOVERY_DEFER_POLICY.deferBaseSeconds * 2 ** index,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PlayRunRecoveryAction = 'engine_retry' | 'defer';
|
|
38
|
+
|
|
39
|
+
export type PlayRunRecoveryReason =
|
|
40
|
+
| 'modal_capacity_exhausted_before_execution'
|
|
41
|
+
| 'modal_circuit_open_before_execution'
|
|
42
|
+
| 'modal_sandbox_unavailable_before_execution'
|
|
43
|
+
| 'modal_runner_start_failed_before_execution'
|
|
44
|
+
| 'runtime_initialization_transport_before_execution'
|
|
45
|
+
| 'runtime_traffic_hold_before_execution'
|
|
46
|
+
| 'runtime_traffic_throttled_before_execution';
|
|
47
|
+
|
|
48
|
+
export type PlayRunRecoveryDecision = Readonly<{
|
|
49
|
+
/** A fresh sandbox is safe only before customer code can have started. */
|
|
50
|
+
stage: 'pre_code';
|
|
51
|
+
/**
|
|
52
|
+
* `engine_retry` consumes the scheduler engine's bounded attempt ladder.
|
|
53
|
+
* `defer` is for dependency-wide pressure and MUST consume the scheduler's
|
|
54
|
+
* durable defer budget/deadline instead of looping in an adapter.
|
|
55
|
+
*/
|
|
56
|
+
action: PlayRunRecoveryAction;
|
|
57
|
+
reason: PlayRunRecoveryReason;
|
|
58
|
+
/** Non-secret scope used by the scheduler's dependency circuit breaker. */
|
|
59
|
+
dependency: 'modal' | 'runtime';
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The scheduler must interpret this error. It is intentionally not a
|
|
64
|
+
* user-facing failure and must not be converted into a PlayRunnerResult:
|
|
65
|
+
* doing so would terminalize work which has not begun executing customer code.
|
|
66
|
+
*/
|
|
67
|
+
export class PlayRunRecoveryError extends Error {
|
|
68
|
+
readonly decision: PlayRunRecoveryDecision;
|
|
69
|
+
readonly cause?: unknown;
|
|
70
|
+
|
|
71
|
+
constructor(input: {
|
|
72
|
+
decision: PlayRunRecoveryDecision;
|
|
73
|
+
message: string;
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
}) {
|
|
76
|
+
super(input.message);
|
|
77
|
+
this.name = PLAY_RUN_RECOVERY_ERROR_NAME;
|
|
78
|
+
this.decision = input.decision;
|
|
79
|
+
if (input.cause !== undefined) {
|
|
80
|
+
this.cause = input.cause;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isPlayRunRecoveryError(
|
|
86
|
+
error: unknown,
|
|
87
|
+
): error is PlayRunRecoveryError {
|
|
88
|
+
return (
|
|
89
|
+
error instanceof Error &&
|
|
90
|
+
error.name === PLAY_RUN_RECOVERY_ERROR_NAME &&
|
|
91
|
+
'decision' in error
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function errorCode(error: unknown): string | number | null {
|
|
96
|
+
if (!error || typeof error !== 'object') return null;
|
|
97
|
+
const code = (error as { code?: unknown }).code;
|
|
98
|
+
return typeof code === 'string' || typeof code === 'number' ? code : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function errorStatus(error: unknown): number | null {
|
|
102
|
+
if (!error || typeof error !== 'object') return null;
|
|
103
|
+
const status = (error as { status?: unknown }).status;
|
|
104
|
+
if (typeof status === 'number' && Number.isFinite(status)) return status;
|
|
105
|
+
const responseStatus = (error as { response?: { status?: unknown } }).response
|
|
106
|
+
?.status;
|
|
107
|
+
return typeof responseStatus === 'number' && Number.isFinite(responseStatus)
|
|
108
|
+
? responseStatus
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function messageOf(error: unknown): string {
|
|
113
|
+
return error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Classify Modal's sandbox-create response at the provider edge, while it is
|
|
118
|
+
* still known that no sandbox/runner/customer code exists. Do not use this
|
|
119
|
+
* classifier after `sandboxes.create` succeeds: a later failure may be
|
|
120
|
+
* ambiguous and must not receive a blind fresh replay.
|
|
121
|
+
*/
|
|
122
|
+
export function recoveryForModalSandboxCreateFailure(
|
|
123
|
+
error: unknown,
|
|
124
|
+
): PlayRunRecoveryError | null {
|
|
125
|
+
const code = errorCode(error);
|
|
126
|
+
const status = errorStatus(error);
|
|
127
|
+
const message = messageOf(error);
|
|
128
|
+
const resourceExhausted =
|
|
129
|
+
code === 8 ||
|
|
130
|
+
(typeof code === 'string' && /RESOURCE_EXHAUSTED/i.test(code)) ||
|
|
131
|
+
/\bRESOURCE_EXHAUSTED\b/i.test(message);
|
|
132
|
+
if (resourceExhausted) {
|
|
133
|
+
return new PlayRunRecoveryError({
|
|
134
|
+
decision: {
|
|
135
|
+
stage: 'pre_code',
|
|
136
|
+
action: 'defer',
|
|
137
|
+
reason: 'modal_capacity_exhausted_before_execution',
|
|
138
|
+
dependency: 'modal',
|
|
139
|
+
},
|
|
140
|
+
message:
|
|
141
|
+
'Modal sandbox capacity is unavailable before customer execution began.',
|
|
142
|
+
cause: error,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const unavailable =
|
|
147
|
+
code === 14 ||
|
|
148
|
+
code === 4 ||
|
|
149
|
+
(typeof code === 'string' &&
|
|
150
|
+
/(?:UNAVAILABLE|DEADLINE_EXCEEDED)/i.test(code)) ||
|
|
151
|
+
status === 429 ||
|
|
152
|
+
(status !== null && status >= 500 && status <= 599) ||
|
|
153
|
+
/\b(?:UNAVAILABLE|DEADLINE_EXCEEDED)\b/i.test(message);
|
|
154
|
+
if (!unavailable) return null;
|
|
155
|
+
return new PlayRunRecoveryError({
|
|
156
|
+
decision: {
|
|
157
|
+
stage: 'pre_code',
|
|
158
|
+
action: 'engine_retry',
|
|
159
|
+
reason: 'modal_sandbox_unavailable_before_execution',
|
|
160
|
+
dependency: 'modal',
|
|
161
|
+
},
|
|
162
|
+
message:
|
|
163
|
+
'Modal sandbox creation was temporarily unavailable before customer execution began.',
|
|
164
|
+
cause: error,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The scheduler circuit blocked provider acquisition before the backend made a
|
|
170
|
+
* Modal API call. This is equivalent to a proven pre-code capacity rejection,
|
|
171
|
+
* but avoids turning a shared-account outage into one create request per run.
|
|
172
|
+
*/
|
|
173
|
+
export function recoveryForOpenModalCircuit(): PlayRunRecoveryError {
|
|
174
|
+
return new PlayRunRecoveryError({
|
|
175
|
+
decision: {
|
|
176
|
+
stage: 'pre_code',
|
|
177
|
+
action: 'defer',
|
|
178
|
+
reason: 'modal_circuit_open_before_execution',
|
|
179
|
+
dependency: 'modal',
|
|
180
|
+
},
|
|
181
|
+
message:
|
|
182
|
+
'Modal sandbox acquisition is deferred while the shared dependency circuit is open.',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* An operator traffic policy is evaluated before a first managed sandbox is
|
|
188
|
+
* created. It becomes a bounded durable recovery defer, never a generic task
|
|
189
|
+
* retry, so a forgotten SEV mitigation cannot spin accepted webhooks forever.
|
|
190
|
+
*/
|
|
191
|
+
export function recoveryForRuntimeTrafficPolicy(input: {
|
|
192
|
+
mode: 'hold' | 'throttle';
|
|
193
|
+
}): PlayRunRecoveryError {
|
|
194
|
+
const reason =
|
|
195
|
+
input.mode === 'hold'
|
|
196
|
+
? 'runtime_traffic_hold_before_execution'
|
|
197
|
+
: 'runtime_traffic_throttled_before_execution';
|
|
198
|
+
return new PlayRunRecoveryError({
|
|
199
|
+
decision: {
|
|
200
|
+
stage: 'pre_code',
|
|
201
|
+
action: 'defer',
|
|
202
|
+
reason,
|
|
203
|
+
dependency: 'runtime',
|
|
204
|
+
},
|
|
205
|
+
message:
|
|
206
|
+
input.mode === 'hold'
|
|
207
|
+
? 'New sandbox starts are temporarily held by the runtime incident policy.'
|
|
208
|
+
: 'New sandbox starts are temporarily paced by the runtime incident policy.',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* This is constructed only by the detached-runner readiness check. That
|
|
214
|
+
* check has not parked the runner or observed scheduler liveness, so it is a
|
|
215
|
+
* pre-code failure for recovery purposes. The scheduler's bounded engine
|
|
216
|
+
* ladder, not this module, chooses how many fresh starts are allowed.
|
|
217
|
+
*/
|
|
218
|
+
export function recoveryForModalRunnerStartupFailure(
|
|
219
|
+
error: unknown,
|
|
220
|
+
): PlayRunRecoveryError {
|
|
221
|
+
return new PlayRunRecoveryError({
|
|
222
|
+
decision: {
|
|
223
|
+
stage: 'pre_code',
|
|
224
|
+
action: 'engine_retry',
|
|
225
|
+
reason: 'modal_runner_start_failed_before_execution',
|
|
226
|
+
dependency: 'modal',
|
|
227
|
+
},
|
|
228
|
+
message:
|
|
229
|
+
'Modal sandbox did not become ready before customer execution began.',
|
|
230
|
+
cause: error,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A runner transport classifier has already proved that the result has no
|
|
236
|
+
* customer rows or steps. Keep its engine retry typed so an ordinary failed
|
|
237
|
+
* result cannot accidentally bypass the engine's bounded retry ladder.
|
|
238
|
+
*/
|
|
239
|
+
export function recoveryForRuntimeInitializationTransportFailure(
|
|
240
|
+
reason:
|
|
241
|
+
| 'runtime_postgres_connect'
|
|
242
|
+
| 'runtime_api_transport'
|
|
243
|
+
| 'runtime_receipt_gateway_transport',
|
|
244
|
+
): PlayRunRecoveryError {
|
|
245
|
+
return new PlayRunRecoveryError({
|
|
246
|
+
decision: {
|
|
247
|
+
stage: 'pre_code',
|
|
248
|
+
action: 'engine_retry',
|
|
249
|
+
reason: 'runtime_initialization_transport_before_execution',
|
|
250
|
+
dependency: 'runtime',
|
|
251
|
+
},
|
|
252
|
+
message: `Runtime initialization transport failed before customer execution began (${reason}).`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -32,7 +32,7 @@ export const INTERNAL_RUNTIME_STORAGE_ERROR_MESSAGE =
|
|
|
32
32
|
export const RUNTIME_SANDBOX_LOST_MESSAGE =
|
|
33
33
|
'The execution sandbox became unreachable before returning a terminal result. Re-run the same command; if this keeps happening, contact Deepline support with the run ID.';
|
|
34
34
|
export const RUNTIME_SANDBOX_OOM_MESSAGE =
|
|
35
|
-
'The execution sandbox ran out of memory. Completed work is durably recorded.
|
|
35
|
+
'The execution sandbox ran out of memory. Completed work is durably recorded. Deepline does not automatically retry this run because the same resource profile is unlikely to succeed. Reduce the batch size or row payload before starting a new run.';
|
|
36
36
|
export const RUNTIME_SANDBOX_KILLED_MESSAGE =
|
|
37
37
|
'The execution sandbox was killed before it could return a result, and the kill reason is unavailable. Completed work is durably recorded. Retry the same command safely; Deepline reuses completed steps and does not repeat their provider calls.';
|
|
38
38
|
export const RUNTIME_LIMIT_EXCEEDED_MESSAGE =
|
|
@@ -385,7 +385,11 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
|
|
|
385
385
|
code: 'RUNTIME_SANDBOX_OOM',
|
|
386
386
|
phase: 'infrastructure',
|
|
387
387
|
message: RUNTIME_SANDBOX_OOM_MESSAGE,
|
|
388
|
-
|
|
388
|
+
// A confirmed runtime OOM is poison work for the current resource
|
|
389
|
+
// profile. Retrying it automatically spends capacity without changing
|
|
390
|
+
// the cause; completed receipts remain available to a deliberate run
|
|
391
|
+
// after the input or limits are changed.
|
|
392
|
+
retryable: false,
|
|
389
393
|
cause,
|
|
390
394
|
};
|
|
391
395
|
}
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
PLAY_RUNNER_STARTUP_GRACE_SECONDS,
|
|
15
15
|
STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
|
|
16
16
|
} from '@shared_libs/play-runtime/runtime-constants';
|
|
17
|
+
import {
|
|
18
|
+
RUNTIME_RELIABILITY_ENV,
|
|
19
|
+
RUNTIME_RELIABILITY_SEV_OVERRIDE_ENV,
|
|
20
|
+
} from '@shared_libs/play-runtime/runtime-reliability-policy';
|
|
17
21
|
import {
|
|
18
22
|
STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
19
23
|
validatePlaySandboxRuntimeLimits,
|
|
@@ -80,6 +84,26 @@ function shellQuote(value: string): string {
|
|
|
80
84
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Sandboxes intentionally receive no general worker environment. The bounded,
|
|
89
|
+
* non-secret reliability controls are the exception: copy only their declared
|
|
90
|
+
* names so an incident override affects the runner's gateway/egress deadlines
|
|
91
|
+
* too, without ever widening this into an environment pass-through.
|
|
92
|
+
*/
|
|
93
|
+
function runtimeReliabilityEnvironmentPrefix(): string {
|
|
94
|
+
const names = [
|
|
95
|
+
RUNTIME_RELIABILITY_SEV_OVERRIDE_ENV,
|
|
96
|
+
...Object.values(RUNTIME_RELIABILITY_ENV),
|
|
97
|
+
];
|
|
98
|
+
const entries = names.flatMap((name) => {
|
|
99
|
+
const value = process.env[name];
|
|
100
|
+
return value === undefined || value.trim() === ''
|
|
101
|
+
? []
|
|
102
|
+
: [`${name}=${shellQuote(value)}`];
|
|
103
|
+
});
|
|
104
|
+
return entries.length > 0 ? `${entries.join(' ')} ` : '';
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
function gzipUtf8(value: string): Buffer {
|
|
84
108
|
return gzipSync(Buffer.from(value, 'utf-8'));
|
|
85
109
|
}
|
|
@@ -326,7 +350,7 @@ async function main() {
|
|
|
326
350
|
code: 'RUNTIME_SANDBOX_OOM',
|
|
327
351
|
phase: 'infrastructure',
|
|
328
352
|
message: ${JSON.stringify(RUNTIME_SANDBOX_OOM_MESSAGE)},
|
|
329
|
-
retryable:
|
|
353
|
+
retryable: false,
|
|
330
354
|
cause: synthesizedError,
|
|
331
355
|
}] }
|
|
332
356
|
: {}),
|
|
@@ -579,6 +603,7 @@ export async function stageRunnerPayload(input: {
|
|
|
579
603
|
process.env.DEEPLINE_RUNTIME_RECEIPT_TRACE === '1'
|
|
580
604
|
? 'DEEPLINE_RUNTIME_RECEIPT_TRACE=1 '
|
|
581
605
|
: '';
|
|
606
|
+
const runtimeReliabilityEnv = runtimeReliabilityEnvironmentPrefix();
|
|
582
607
|
const watchedRunnerCommand = [
|
|
583
608
|
'node',
|
|
584
609
|
'-e',
|
|
@@ -600,7 +625,7 @@ export async function stageRunnerPayload(input: {
|
|
|
600
625
|
artifactCodePath,
|
|
601
626
|
artifactSourceMapPath,
|
|
602
627
|
crashPusherPath,
|
|
603
|
-
})} && DEEPLINE_PLAY_RUNNER_RUNTIME_STARTED_PATH=${shellQuote(runtimeStartedPath)} DEEPLINE_PLAY_RUNNER_RUNTIME_COMPLETED_PATH=${shellQuote(runtimeCompletedPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STARTUP_DIAGNOSTIC_PATH=${shellQuote(startupDiagnosticPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}${watchedRunnerCommand}`;
|
|
628
|
+
})} && DEEPLINE_PLAY_RUNNER_RUNTIME_STARTED_PATH=${shellQuote(runtimeStartedPath)} DEEPLINE_PLAY_RUNNER_RUNTIME_COMPLETED_PATH=${shellQuote(runtimeCompletedPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STARTUP_DIAGNOSTIC_PATH=${shellQuote(startupDiagnosticPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runtimeReliabilityEnv}${runnerTraceEnv}${watchedRunnerCommand}`;
|
|
604
629
|
// Crash-containment epilogue: runs UNCONDITIONALLY after the runner exits and
|
|
605
630
|
// pushes the parsed (or synthesized) terminal to the gateway so the parked
|
|
606
631
|
// worker wakes within seconds of ANY runner death — process.exit abuse, OOM
|
|
@@ -609,7 +634,7 @@ export async function stageRunnerPayload(input: {
|
|
|
609
634
|
// Its own retry diagnostics are kept in a separate log: appending them to
|
|
610
635
|
// `outputPath` after a near-gateway-sized result could hide that result from
|
|
611
636
|
// a later bounded-tail recovery read.
|
|
612
|
-
const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(crashPusherLogPath)} ${shellQuote(exitCodePath)} ${shellQuote(oomKillBaselinePath)} ${shellQuote(runtimeStartedPath)} ${shellQuote(runtimeCompletedPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(terminationDiagnosticPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)} ${shellQuote(startupDiagnosticPath)}; ( awk '$1 == "oom_kill" { print $2 }' /sys/fs/cgroup/memory.events 2>/dev/null || true ) > ${shellQuote(oomKillBaselinePath)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(oomKillBaselinePath)} /sys/fs/cgroup/memory.events ${shellQuote(terminationDiagnosticPath)} > ${shellQuote(crashPusherLogPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
|
|
637
|
+
const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(crashPusherLogPath)} ${shellQuote(exitCodePath)} ${shellQuote(oomKillBaselinePath)} ${shellQuote(runtimeStartedPath)} ${shellQuote(runtimeCompletedPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(terminationDiagnosticPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)} ${shellQuote(startupDiagnosticPath)}; ( awk '$1 == "oom_kill" { print $2 }' /sys/fs/cgroup/memory.events 2>/dev/null || true ) > ${shellQuote(oomKillBaselinePath)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; ${runtimeReliabilityEnv}node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(oomKillBaselinePath)} /sys/fs/cgroup/memory.events ${shellQuote(terminationDiagnosticPath)} > ${shellQuote(crashPusherLogPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
|
|
613
638
|
|
|
614
639
|
return {
|
|
615
640
|
workDir: input.workDir,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DaytonaSandbox } from './daytona-lifecycle';
|
|
2
|
+
import { RUNTIME_RELIABILITY_POLICY } from '@shared_libs/play-runtime/runtime-reliability-policy';
|
|
2
3
|
|
|
3
4
|
export type DetachedDaytonaStartupDiagnostic = {
|
|
4
5
|
schedulerReadinessObserved: boolean;
|
|
@@ -229,7 +230,8 @@ export type DetachedDaytonaRunnerSupervisor = {
|
|
|
229
230
|
|
|
230
231
|
// The durable heartbeat proves runner-process liveness plus gateway reachability.
|
|
231
232
|
// Keep the handoff bounded while allowing a cold gateway to recover.
|
|
232
|
-
export const DAYTONA_RUNNER_READY_TIMEOUT_MS =
|
|
233
|
+
export const DAYTONA_RUNNER_READY_TIMEOUT_MS =
|
|
234
|
+
RUNTIME_RELIABILITY_POLICY.sandbox.runnerReadyTimeoutMs;
|
|
233
235
|
const DAYTONA_RUNNER_READY_POLL_MS = 50;
|
|
234
236
|
|
|
235
237
|
export type DetachedRunnerReadinessPort = (
|
|
@@ -41,9 +41,12 @@ import {
|
|
|
41
41
|
DaytonaRunnerInitializationError,
|
|
42
42
|
prepareDetachedDaytonaRunner,
|
|
43
43
|
} from './daytona-session-execution';
|
|
44
|
+
import { RUNTIME_RELIABILITY_POLICY } from '@shared_libs/play-runtime/runtime-reliability-policy';
|
|
44
45
|
|
|
45
|
-
const DAYTONA_COMMAND_RECOVERY_TIMEOUT_MS =
|
|
46
|
-
|
|
46
|
+
const DAYTONA_COMMAND_RECOVERY_TIMEOUT_MS =
|
|
47
|
+
RUNTIME_RELIABILITY_POLICY.sandbox.daytonaCommandRecoveryTimeoutMs;
|
|
48
|
+
const DAYTONA_COMMAND_RECOVERY_POLL_MS =
|
|
49
|
+
RUNTIME_RELIABILITY_POLICY.sandbox.daytonaCommandRecoveryPollMs;
|
|
47
50
|
const DAYTONA_INFRASTRUCTURE_MAX_ATTEMPTS = 2;
|
|
48
51
|
const DAYTONA_UPLOAD_MAX_ATTEMPTS = 2;
|
|
49
52
|
const DAYTONA_UPLOAD_ATTEMPT_DEADLINE_MS = 90_000;
|