deepline 0.1.252 → 0.1.254
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 +8 -7
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +261 -162
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +37 -35
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +20 -5
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +462 -29
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +7 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +98 -13
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +117 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +19 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/secret-resolution-retry-policy.ts +80 -0
- package/dist/cli/index.js +80 -51
- package/dist/cli/index.mjs +80 -51
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -10
- package/dist/index.mjs +13 -10
- package/package.json +1 -1
|
@@ -236,10 +236,26 @@ export const RUNNER_TERMINAL_CHECKPOINT_DISPOSITION = {
|
|
|
236
236
|
export type RunnerTerminalCheckpointDisposition =
|
|
237
237
|
(typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION)[keyof typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION];
|
|
238
238
|
|
|
239
|
+
export type PlayRunOutputWarning = {
|
|
240
|
+
code: 'RUN_OUTPUT_TRUNCATED';
|
|
241
|
+
reason: 'array_preview_limit' | 'byte_limit';
|
|
242
|
+
message: string;
|
|
243
|
+
originalBytes?: number;
|
|
244
|
+
originalBytesAtLeast?: number;
|
|
245
|
+
retainedBytes?: number;
|
|
246
|
+
limitBytes?: number;
|
|
247
|
+
limitItems?: number;
|
|
248
|
+
originalItems?: number;
|
|
249
|
+
retainedItems?: number;
|
|
250
|
+
retryable: false;
|
|
251
|
+
recoverableFrom: 'runtime_sheet' | 'none';
|
|
252
|
+
};
|
|
253
|
+
|
|
239
254
|
export type PlayRunnerResult =
|
|
240
255
|
| {
|
|
241
256
|
status: 'completed';
|
|
242
257
|
output: unknown;
|
|
258
|
+
outputWarnings?: PlayRunOutputWarning[];
|
|
243
259
|
outputRowCount?: number;
|
|
244
260
|
logs: string[];
|
|
245
261
|
stats: Record<string, unknown>;
|
|
@@ -58,6 +58,10 @@ export interface RuntimeResourceGovernor {
|
|
|
58
58
|
estimatedCost?: number | null;
|
|
59
59
|
signal?: AbortSignal;
|
|
60
60
|
}): Promise<RuntimeResourceLease>;
|
|
61
|
+
acquireProviderPermit(input: {
|
|
62
|
+
toolId: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<RuntimeResourceLease>;
|
|
61
65
|
acquireSheetFlush(input?: {
|
|
62
66
|
estimatedBytes?: number | null;
|
|
63
67
|
rowCount?: number | null;
|
|
@@ -290,6 +294,13 @@ export function createRuntimeResourceGovernor(input: {
|
|
|
290
294
|
return lease;
|
|
291
295
|
},
|
|
292
296
|
|
|
297
|
+
acquireProviderPermit(providerInput) {
|
|
298
|
+
return input.executionGovernor.acquireProviderPermit(
|
|
299
|
+
providerInput.toolId,
|
|
300
|
+
{ signal: providerInput.signal },
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
|
|
293
304
|
async acquireSheetFlush(sheetInput) {
|
|
294
305
|
const startedAt = Date.now();
|
|
295
306
|
const lease = await sheetFlushAdmission.acquire(sheetInput?.signal);
|
|
@@ -6,7 +6,11 @@ const CLOUDFLARE_WORKFLOW_ENDPOINT_UNAVAILABLE_RE =
|
|
|
6
6
|
/The requested endpoint could not be found, or you don't have access to it\. Please check the provided ID and try again\./i;
|
|
7
7
|
const VERCEL_RUNTIME_API_DEPLOYMENT_MISSING_RE =
|
|
8
8
|
/runtime API 404:[\s\S]*(?:DeploymentNotFound|DEPLOYMENT_NOT_FOUND|requested deployment .*exist|deployment could not be found on Vercel)/i;
|
|
9
|
+
const RUNTIME_LIMIT_EXCEEDED_RE = /\bRUNTIME_LIMIT_EXCEEDED\b/i;
|
|
10
|
+
const RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_RE =
|
|
11
|
+
/\bRUNTIME_SANDBOX_INSPECTION_UNAVAILABLE\b/i;
|
|
9
12
|
const RUNTIME_SANDBOX_LOST_RE = /\bRUNTIME_SANDBOX_LOST\b/i;
|
|
13
|
+
const OUTPUT_TOO_LARGE_RE = /\b(?:OUTPUT_TOO_LARGE|OutputTooLarge)\b/;
|
|
10
14
|
|
|
11
15
|
export const PLATFORM_DEPLOY_INTERRUPTED_MESSAGE =
|
|
12
16
|
'Run interrupted by a platform deploy. Deepline retries this automatically when possible; if this error is still visible, re-run the same command.';
|
|
@@ -19,6 +23,10 @@ export const INTERNAL_RUNTIME_STORAGE_ERROR_MESSAGE =
|
|
|
19
23
|
'Internal play runtime storage failed. Please retry the run; if this keeps happening, contact Deepline support with the run ID.';
|
|
20
24
|
export const RUNTIME_SANDBOX_LOST_MESSAGE =
|
|
21
25
|
'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.';
|
|
26
|
+
export const RUNTIME_LIMIT_EXCEEDED_MESSAGE =
|
|
27
|
+
'The play reached its 30 minute runtime limit and was stopped. Completed row state was preserved; run a smaller batch or continue from the persisted rows.';
|
|
28
|
+
export const RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_MESSAGE =
|
|
29
|
+
'The play stopped after Deepline could not verify the execution sandbox state. Completed row state was preserved. Re-run from the persisted rows; if this keeps happening, contact Deepline support with the run ID.';
|
|
22
30
|
|
|
23
31
|
export const WORKSPACE_STORAGE_NOT_READY_CODE = 'WORKSPACE_STORAGE_NOT_READY';
|
|
24
32
|
|
|
@@ -164,7 +172,10 @@ function boundedFailureText(value: string): string {
|
|
|
164
172
|
function boundedFailureStack(error: Error): string | undefined {
|
|
165
173
|
if (typeof error.stack !== 'string' || !error.stack.trim()) return undefined;
|
|
166
174
|
return boundedFailureText(
|
|
167
|
-
error.stack
|
|
175
|
+
error.stack
|
|
176
|
+
.split('\n')
|
|
177
|
+
.slice(0, PUBLIC_FAILURE_STACK_LINE_LIMIT)
|
|
178
|
+
.join('\n'),
|
|
168
179
|
);
|
|
169
180
|
}
|
|
170
181
|
|
|
@@ -205,6 +216,38 @@ function toErrorText(error: unknown): string {
|
|
|
205
216
|
export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
|
|
206
217
|
const rawCause = toErrorText(error);
|
|
207
218
|
const cause = boundedFailureText(rawCause);
|
|
219
|
+
if (
|
|
220
|
+
(error &&
|
|
221
|
+
typeof error === 'object' &&
|
|
222
|
+
(error as { code?: unknown }).code === 'OUTPUT_TOO_LARGE') ||
|
|
223
|
+
OUTPUT_TOO_LARGE_RE.test(rawCause)
|
|
224
|
+
) {
|
|
225
|
+
return {
|
|
226
|
+
code: 'OUTPUT_TOO_LARGE',
|
|
227
|
+
phase: 'runtime',
|
|
228
|
+
message: cause,
|
|
229
|
+
retryable: false,
|
|
230
|
+
cause,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (RUNTIME_LIMIT_EXCEEDED_RE.test(rawCause)) {
|
|
234
|
+
return {
|
|
235
|
+
code: 'RUNTIME_LIMIT_EXCEEDED',
|
|
236
|
+
phase: 'runtime',
|
|
237
|
+
message: RUNTIME_LIMIT_EXCEEDED_MESSAGE,
|
|
238
|
+
retryable: false,
|
|
239
|
+
cause,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_RE.test(rawCause)) {
|
|
243
|
+
return {
|
|
244
|
+
code: 'RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE',
|
|
245
|
+
phase: 'infrastructure',
|
|
246
|
+
message: RUNTIME_SANDBOX_INSPECTION_UNAVAILABLE_MESSAGE,
|
|
247
|
+
retryable: false,
|
|
248
|
+
cause,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
208
251
|
if (RUNTIME_SANDBOX_LOST_RE.test(rawCause)) {
|
|
209
252
|
const stack = error instanceof Error ? boundedFailureStack(error) : null;
|
|
210
253
|
const causes = error instanceof Error ? failureCauseTexts(error) : [];
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -4,13 +4,16 @@ import type {
|
|
|
4
4
|
PlayRunnerResult,
|
|
5
5
|
} from '@shared_libs/play-runtime/protocol';
|
|
6
6
|
import { isIsolatedRuntimeSchedulerSchema } from '@shared_libs/play-runtime/runtime-scheduler-topology';
|
|
7
|
+
import { PLAY_RUNNER_TIMEOUT_SECONDS } from '@shared_libs/play-runtime/runtime-constants';
|
|
7
8
|
|
|
8
9
|
const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
|
|
9
10
|
const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
const DAYTONA_AUTO_STOP_INTERVAL_MINUTES =
|
|
11
|
+
// Explicit runner deadline + scheduler GC own the normal lifecycle. Daytona's
|
|
12
|
+
// inactivity stop is a wider crash backstop measured from sandbox creation, so
|
|
13
|
+
// setup time cannot consume the terminal-flush grace.
|
|
14
|
+
const DAYTONA_AUTO_STOP_INTERVAL_MINUTES = Math.ceil(
|
|
15
|
+
PLAY_RUNNER_TIMEOUT_SECONDS / 60,
|
|
16
|
+
);
|
|
14
17
|
const DAYTONA_AUTO_ARCHIVE_INTERVAL_MINUTES = 15;
|
|
15
18
|
const DAYTONA_SANDBOX_LABEL_SOURCE = 'deepline-play-runner';
|
|
16
19
|
// The sandbox must be created at its final resource boundary. Do not add a
|
|
@@ -9,12 +9,22 @@ import {
|
|
|
9
9
|
PLAY_RUNTIME_CONTRACT,
|
|
10
10
|
PLAY_RUNTIME_CONTRACT_HEADER,
|
|
11
11
|
} from '@shared_libs/play-runtime/runtime-contract';
|
|
12
|
+
import {
|
|
13
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
14
|
+
PLAY_RUNNER_STARTUP_GRACE_SECONDS,
|
|
15
|
+
STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
|
|
16
|
+
} from '@shared_libs/play-runtime/runtime-constants';
|
|
17
|
+
import {
|
|
18
|
+
RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES,
|
|
19
|
+
RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES,
|
|
20
|
+
} from '@shared_libs/play-runtime/output-size-limits';
|
|
12
21
|
import { compactPlayArtifactForRuntimeTransport } from '@shared_libs/plays/artifact-transport';
|
|
13
22
|
import type {
|
|
14
23
|
DaytonaExecutionContext,
|
|
15
24
|
DaytonaSandbox,
|
|
16
25
|
DaytonaStageEmitter,
|
|
17
26
|
} from './daytona-lifecycle';
|
|
27
|
+
import { buildDaytonaRuntimeWatchdogSource } from './daytona-runtime-watchdog';
|
|
18
28
|
|
|
19
29
|
type DaytonaRemoteRuntimeContext = PlayRunnerExecutionConfig['context'];
|
|
20
30
|
|
|
@@ -28,6 +38,20 @@ type DaytonaPayloadEnvelope = {
|
|
|
28
38
|
crashPusherCode: string;
|
|
29
39
|
};
|
|
30
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The crash pusher must be able to recover one complete terminal event after
|
|
43
|
+
* a runner exits before its primary gateway push. A stdout `result` event is
|
|
44
|
+
* smaller than the accepted runner-terminal request envelope (the request also
|
|
45
|
+
* carries action + runId), so this covers every terminal that the gateway can
|
|
46
|
+
* have accepted, including a resume-required suspended checkpoint. Leave room
|
|
47
|
+
* for the runner's primary-push retry diagnostics written after that event.
|
|
48
|
+
*/
|
|
49
|
+
export const DAYTONA_CRASH_PUSHER_POST_EVENT_DIAGNOSTIC_HEADROOM_BYTES =
|
|
50
|
+
RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES;
|
|
51
|
+
export const DAYTONA_CRASH_PUSHER_OUTPUT_TAIL_MAX_BYTES =
|
|
52
|
+
RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES +
|
|
53
|
+
DAYTONA_CRASH_PUSHER_POST_EVENT_DIAGNOSTIC_HEADROOM_BYTES;
|
|
54
|
+
|
|
31
55
|
export type StagedDaytonaPayload = {
|
|
32
56
|
workDir: string;
|
|
33
57
|
command: string;
|
|
@@ -99,8 +123,32 @@ const fs = require('node:fs');
|
|
|
99
123
|
const configPath = process.argv[2];
|
|
100
124
|
const exitCodeRaw = Number.parseInt(process.argv[3] ?? '', 10);
|
|
101
125
|
const outputPath = process.argv[4];
|
|
126
|
+
const runtimeLimitMarkerPath = process.argv[5] || '';
|
|
102
127
|
const exitCode = Number.isFinite(exitCodeRaw) ? exitCodeRaw : null;
|
|
103
|
-
const MAX_OUTPUT_BYTES =
|
|
128
|
+
const MAX_OUTPUT_BYTES = ${DAYTONA_CRASH_PUSHER_OUTPUT_TAIL_MAX_BYTES};
|
|
129
|
+
const MAX_POST_TERMINAL_DIAGNOSTIC_BYTES = ${RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES};
|
|
130
|
+
let postTerminalDiagnosticBytesRemaining = MAX_POST_TERMINAL_DIAGNOSTIC_BYTES;
|
|
131
|
+
|
|
132
|
+
function truncateUtf8(value, maxBytes) {
|
|
133
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
|
|
134
|
+
const encoded = Buffer.from(value, 'utf8');
|
|
135
|
+
for (let end = Math.min(encoded.length, maxBytes); end > 0; end -= 1) {
|
|
136
|
+
const candidate = encoded.subarray(0, end).toString('utf8');
|
|
137
|
+
if (Buffer.byteLength(candidate, 'utf8') <= maxBytes) return candidate;
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function writeDiagnostic(line) {
|
|
143
|
+
if (postTerminalDiagnosticBytesRemaining <= 1) return;
|
|
144
|
+
const bounded = truncateUtf8(
|
|
145
|
+
String(line),
|
|
146
|
+
postTerminalDiagnosticBytesRemaining - 1,
|
|
147
|
+
);
|
|
148
|
+
if (!bounded) return;
|
|
149
|
+
console.log(bounded);
|
|
150
|
+
postTerminalDiagnosticBytesRemaining -= Buffer.byteLength(bounded, 'utf8') + 1;
|
|
151
|
+
}
|
|
104
152
|
|
|
105
153
|
function readResultFromOutput() {
|
|
106
154
|
try {
|
|
@@ -170,17 +218,34 @@ async function main() {
|
|
|
170
218
|
const gateway = String(context.receiptGatewayBaseUrl || '').replace(/\\/$/, '');
|
|
171
219
|
const token = context.executorToken;
|
|
172
220
|
if (!push || !push.runId || !gateway || !token) {
|
|
173
|
-
|
|
221
|
+
writeDiagnostic('[crash-terminal] no push config; skipping');
|
|
174
222
|
return;
|
|
175
223
|
}
|
|
176
224
|
const parsedResult = readResultFromOutput();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
225
|
+
// Exit codes are authored-program output too: a play may intentionally call
|
|
226
|
+
// process.exit(124). Only the separate parent watchdog writes this marker.
|
|
227
|
+
const runtimeLimitExceeded = Boolean(
|
|
228
|
+
runtimeLimitMarkerPath && fs.existsSync(runtimeLimitMarkerPath),
|
|
229
|
+
);
|
|
230
|
+
const synthesizedError = runtimeLimitExceeded
|
|
231
|
+
? 'RUNTIME_LIMIT_EXCEEDED: The play reached its 30 minute runtime limit and was stopped.'
|
|
232
|
+
:
|
|
180
233
|
'Daytona play runner exited with code ' +
|
|
181
234
|
(exitCode === null ? 'unknown' : exitCode) +
|
|
182
235
|
(exitCode === 137 ? ' (sandbox OOM or forced SIGKILL)' : '') +
|
|
183
|
-
' without pushing a terminal (crash containment epilogue).'
|
|
236
|
+
' without pushing a terminal (crash containment epilogue).';
|
|
237
|
+
const result = terminalResultForTransport(parsedResult || {
|
|
238
|
+
status: 'failed',
|
|
239
|
+
error: synthesizedError,
|
|
240
|
+
errors: runtimeLimitExceeded
|
|
241
|
+
? [{
|
|
242
|
+
code: 'RUNTIME_LIMIT_EXCEEDED',
|
|
243
|
+
phase: 'runtime',
|
|
244
|
+
message: 'The play reached its 30 minute runtime limit and was stopped. Completed row state was preserved; run a smaller batch or continue from the persisted rows.',
|
|
245
|
+
retryable: false,
|
|
246
|
+
cause: synthesizedError,
|
|
247
|
+
}]
|
|
248
|
+
: undefined,
|
|
184
249
|
logs: [],
|
|
185
250
|
stats: {},
|
|
186
251
|
steps: [],
|
|
@@ -209,24 +274,24 @@ async function main() {
|
|
|
209
274
|
});
|
|
210
275
|
clearTimeout(timer);
|
|
211
276
|
if (response.ok) {
|
|
212
|
-
|
|
277
|
+
writeDiagnostic('[crash-terminal] pushed status=' + result.status);
|
|
213
278
|
return;
|
|
214
279
|
}
|
|
215
|
-
|
|
280
|
+
writeDiagnostic('[crash-terminal] rejected http=' + response.status);
|
|
216
281
|
// Deterministic scope/fence rejections cannot succeed on retry.
|
|
217
282
|
if (response.status >= 400 && response.status < 500) return;
|
|
218
283
|
} catch (error) {
|
|
219
|
-
|
|
284
|
+
writeDiagnostic(
|
|
220
285
|
'[crash-terminal] transport ' + (error && error.message ? error.message : String(error)),
|
|
221
286
|
);
|
|
222
287
|
}
|
|
223
288
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, attempt * 1000));
|
|
224
289
|
}
|
|
225
|
-
|
|
290
|
+
writeDiagnostic('[crash-terminal] exhausted retries');
|
|
226
291
|
}
|
|
227
292
|
|
|
228
293
|
main().catch((error) => {
|
|
229
|
-
|
|
294
|
+
writeDiagnostic('[crash-terminal] fatal ' + (error && error.message ? error.message : String(error)));
|
|
230
295
|
});
|
|
231
296
|
`;
|
|
232
297
|
}
|
|
@@ -389,14 +454,31 @@ export async function stageDaytonaRunnerPayload(input: {
|
|
|
389
454
|
});
|
|
390
455
|
|
|
391
456
|
const outputPath = `${workDir}/deepline-play-output-${randomUUID()}.log`;
|
|
457
|
+
const crashPusherLogPath = `${workDir}/deepline-play-crash-terminal-${randomUUID()}.log`;
|
|
392
458
|
const exitCodePath = `${workDir}/deepline-play-exit-${randomUUID()}.txt`;
|
|
393
459
|
const readyPath = `${workDir}/deepline-play-ready-${randomUUID()}.json`;
|
|
394
460
|
const startAckPath = `${workDir}/deepline-play-start-ack-${randomUUID()}.json`;
|
|
461
|
+
const runtimeStartedPath = `${workDir}/deepline-play-runtime-started-${randomUUID()}.json`;
|
|
462
|
+
const runtimeCompletedPath = `${workDir}/deepline-play-runtime-completed-${randomUUID()}.json`;
|
|
463
|
+
const runtimeLimitMarkerPath = `${workDir}/deepline-play-runtime-limit-${randomUUID()}.txt`;
|
|
395
464
|
const progressEventPath = `${workDir}/deepline-play-progress-${randomUUID()}.jsonl`;
|
|
396
465
|
const runnerTraceEnv =
|
|
397
466
|
process.env.DEEPLINE_RUNTIME_RECEIPT_TRACE === '1'
|
|
398
467
|
? 'DEEPLINE_RUNTIME_RECEIPT_TRACE=1 '
|
|
399
468
|
: '';
|
|
469
|
+
const watchedRunnerCommand = [
|
|
470
|
+
'node',
|
|
471
|
+
'-e',
|
|
472
|
+
shellQuote(buildDaytonaRuntimeWatchdogSource()),
|
|
473
|
+
shellQuote(runnerPath),
|
|
474
|
+
shellQuote(configPath),
|
|
475
|
+
shellQuote(runtimeStartedPath),
|
|
476
|
+
shellQuote(runtimeCompletedPath),
|
|
477
|
+
String(STANDARD_PLAY_RUNTIME_LIMIT_SECONDS * 1_000),
|
|
478
|
+
String(PLAY_RUNNER_STARTUP_GRACE_SECONDS * 1_000),
|
|
479
|
+
String(PLAY_RUNNER_TERMINAL_GRACE_SECONDS * 1_000),
|
|
480
|
+
shellQuote(runtimeLimitMarkerPath),
|
|
481
|
+
].join(' ');
|
|
400
482
|
const runnerCommand = `${nodeMaterializePayloadCommand({
|
|
401
483
|
envelopePath,
|
|
402
484
|
runnerPath,
|
|
@@ -404,13 +486,16 @@ export async function stageDaytonaRunnerPayload(input: {
|
|
|
404
486
|
artifactCodePath,
|
|
405
487
|
artifactSourceMapPath,
|
|
406
488
|
crashPusherPath,
|
|
407
|
-
})} && DEEPLINE_PLAY_RUNNER_READY_PATH=${shellQuote(readyPath)} DEEPLINE_PLAY_RUNNER_START_ACK_PATH=${shellQuote(startAckPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}
|
|
489
|
+
})} && DEEPLINE_PLAY_RUNNER_READY_PATH=${shellQuote(readyPath)} DEEPLINE_PLAY_RUNNER_START_ACK_PATH=${shellQuote(startAckPath)} 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_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}${watchedRunnerCommand}`;
|
|
408
490
|
// Crash-containment epilogue: runs UNCONDITIONALLY after the runner exits and
|
|
409
491
|
// pushes the parsed (or synthesized) terminal to the gateway so the parked
|
|
410
492
|
// worker wakes within seconds of ANY runner death — process.exit abuse, OOM
|
|
411
493
|
// SIGKILL, or a crash before the in-process push. Idempotent against a
|
|
412
494
|
// successful in-runner push (terminals are first-write-wins).
|
|
413
|
-
|
|
495
|
+
// Its own retry diagnostics are kept in a separate log: appending them to
|
|
496
|
+
// `outputPath` after a near-gateway-sized result could hide that result from
|
|
497
|
+
// a later bounded-tail recovery read.
|
|
498
|
+
const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(crashPusherLogPath)} ${shellQuote(exitCodePath)} ${shellQuote(readyPath)} ${shellQuote(startAckPath)} ${shellQuote(runtimeStartedPath)} ${shellQuote(runtimeCompletedPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} ${shellQuote(runtimeLimitMarkerPath)} > ${shellQuote(crashPusherLogPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
|
|
414
499
|
|
|
415
500
|
return {
|
|
416
501
|
workDir: input.workDir,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Exit code reserved for the external runner watchdog's runtime deadline. */
|
|
2
|
+
export const DAYTONA_RUNTIME_WATCHDOG_EXIT_CODE = 124;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the tiny parent process that owns the hard runtime deadline outside
|
|
6
|
+
* the customer-code Node event loop. A synchronous infinite loop can block the
|
|
7
|
+
* runner's own timer, but it cannot block this separate process.
|
|
8
|
+
*/
|
|
9
|
+
export function buildDaytonaRuntimeWatchdogSource(): string {
|
|
10
|
+
return `
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const { spawn } = require('node:child_process');
|
|
13
|
+
const runnerPath = process.argv[1];
|
|
14
|
+
const configPath = process.argv[2];
|
|
15
|
+
const runtimeStartedPath = process.argv[3];
|
|
16
|
+
const runtimeCompletedPath = process.argv[4];
|
|
17
|
+
const limitMs = Math.max(1, Number.parseInt(process.argv[5] || '', 10) || 1);
|
|
18
|
+
const startupLimitMs = Math.max(1, Number.parseInt(process.argv[6] || '', 10) || 1);
|
|
19
|
+
const terminalGraceMs = Math.max(1, Number.parseInt(process.argv[7] || '', 10) || 1);
|
|
20
|
+
const runtimeLimitMarkerPath = process.argv[8] || '';
|
|
21
|
+
const child = spawn(process.execPath, [runnerPath, configPath], {
|
|
22
|
+
env: process.env,
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
});
|
|
25
|
+
let timedOut = false;
|
|
26
|
+
let startupTimedOut = false;
|
|
27
|
+
let terminalFlushTimedOut = false;
|
|
28
|
+
let settled = false;
|
|
29
|
+
let forceKillTimer = null;
|
|
30
|
+
let runtimeTimer = null;
|
|
31
|
+
let terminalGraceTimer = null;
|
|
32
|
+
|
|
33
|
+
function stopChild() {
|
|
34
|
+
child.kill('SIGTERM');
|
|
35
|
+
forceKillTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function markRuntimeLimit() {
|
|
39
|
+
if (!runtimeLimitMarkerPath) return;
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(runtimeLimitMarkerPath, 'runtime_limit');
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function armRuntimeDeadline() {
|
|
46
|
+
if (settled || runtimeTimer) return;
|
|
47
|
+
clearTimeout(startupTimer);
|
|
48
|
+
runtimeTimer = setTimeout(() => {
|
|
49
|
+
// Close the polling race at the exact deadline: customer code may have
|
|
50
|
+
// written the completion fence after the last 25ms poll.
|
|
51
|
+
if (fs.existsSync(runtimeCompletedPath)) {
|
|
52
|
+
armTerminalGrace();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
timedOut = true;
|
|
56
|
+
markRuntimeLimit();
|
|
57
|
+
process.stderr.write('RUNTIME_LIMIT_EXCEEDED: external runtime watchdog stopped the play.\\n');
|
|
58
|
+
stopChild();
|
|
59
|
+
}, limitMs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function armTerminalGrace() {
|
|
63
|
+
if (settled || terminalGraceTimer) return;
|
|
64
|
+
clearInterval(runtimeBoundaryPoll);
|
|
65
|
+
clearTimeout(startupTimer);
|
|
66
|
+
if (runtimeTimer) {
|
|
67
|
+
clearTimeout(runtimeTimer);
|
|
68
|
+
runtimeTimer = null;
|
|
69
|
+
}
|
|
70
|
+
terminalGraceTimer = setTimeout(() => {
|
|
71
|
+
terminalFlushTimedOut = true;
|
|
72
|
+
process.stderr.write('RUNTIME_TERMINAL_FLUSH_LIMIT_EXCEEDED: terminal settlement exceeded its grace period.\\n');
|
|
73
|
+
stopChild();
|
|
74
|
+
}, terminalGraceMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const runtimeBoundaryPoll = setInterval(() => {
|
|
78
|
+
if (fs.existsSync(runtimeStartedPath)) armRuntimeDeadline();
|
|
79
|
+
if (fs.existsSync(runtimeCompletedPath)) armTerminalGrace();
|
|
80
|
+
}, 25);
|
|
81
|
+
const startupTimer = setTimeout(() => {
|
|
82
|
+
// The startup marker can land between the last poll and this callback.
|
|
83
|
+
if (fs.existsSync(runtimeStartedPath)) {
|
|
84
|
+
armRuntimeDeadline();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
startupTimedOut = true;
|
|
88
|
+
clearInterval(runtimeBoundaryPoll);
|
|
89
|
+
process.stderr.write('RUNTIME_STARTUP_LIMIT_EXCEEDED: runner never reached the customer-code boundary.\\n');
|
|
90
|
+
stopChild();
|
|
91
|
+
}, startupLimitMs);
|
|
92
|
+
if (fs.existsSync(runtimeStartedPath)) armRuntimeDeadline();
|
|
93
|
+
if (fs.existsSync(runtimeCompletedPath)) armTerminalGrace();
|
|
94
|
+
|
|
95
|
+
function finish(code) {
|
|
96
|
+
if (settled) return;
|
|
97
|
+
settled = true;
|
|
98
|
+
clearInterval(runtimeBoundaryPoll);
|
|
99
|
+
clearTimeout(startupTimer);
|
|
100
|
+
if (runtimeTimer) clearTimeout(runtimeTimer);
|
|
101
|
+
if (terminalGraceTimer) clearTimeout(terminalGraceTimer);
|
|
102
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
103
|
+
process.exitCode = timedOut
|
|
104
|
+
? ${DAYTONA_RUNTIME_WATCHDOG_EXIT_CODE}
|
|
105
|
+
: startupTimedOut
|
|
106
|
+
? 1
|
|
107
|
+
: terminalFlushTimedOut
|
|
108
|
+
? 1
|
|
109
|
+
: typeof code === 'number'
|
|
110
|
+
? code
|
|
111
|
+
: 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
child.once('error', () => finish(1));
|
|
115
|
+
child.once('exit', (code) => finish(code));
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
PlayRunnerResult,
|
|
15
15
|
} from '@shared_libs/play-runtime/protocol';
|
|
16
16
|
import {
|
|
17
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
17
18
|
STANDARD_PLAY_RUNTIME_LIMIT_LABEL,
|
|
18
19
|
STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
|
|
19
20
|
} from '@shared_libs/play-runtime/runtime-constants';
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
} from './daytona-session-execution';
|
|
40
41
|
|
|
41
42
|
const DAYTONA_EXECUTE_TIMEOUT_SECONDS = STANDARD_PLAY_RUNTIME_LIMIT_SECONDS;
|
|
43
|
+
const DAYTONA_DETACHED_CEILING_SECONDS =
|
|
44
|
+
STANDARD_PLAY_RUNTIME_LIMIT_SECONDS + PLAY_RUNNER_TERMINAL_GRACE_SECONDS;
|
|
42
45
|
const STANDARD_WORKFLOW_RUNTIME_LIMIT_ERROR = `Based on this plan, max runtime is ${STANDARD_PLAY_RUNTIME_LIMIT_LABEL}. Use smaller batches; ask for runtime.`;
|
|
43
46
|
const DAYTONA_COMMAND_RECOVERY_TIMEOUT_MS = 60_000;
|
|
44
47
|
const DAYTONA_COMMAND_RECOVERY_POLL_MS = 2_000;
|
|
@@ -209,8 +212,10 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
209
212
|
diagnosis: {
|
|
210
213
|
stage:
|
|
211
214
|
| 'salvaged'
|
|
215
|
+
| 'command_running'
|
|
212
216
|
| 'result_missing'
|
|
213
217
|
| 'command_inspection_failed'
|
|
218
|
+
| 'sandbox_missing'
|
|
214
219
|
| 'sandbox_lookup_failed';
|
|
215
220
|
detail: string | null;
|
|
216
221
|
};
|
|
@@ -220,12 +225,14 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
220
225
|
const daytona = daytonaSdkClientFactory.createFull(clientOptions);
|
|
221
226
|
const sandbox = (await daytona.get(input.sandboxId)) as DaytonaSandbox;
|
|
222
227
|
let exitCode: number | null = null;
|
|
228
|
+
let commandFound = false;
|
|
223
229
|
let commandInspectionError: string | null = null;
|
|
224
230
|
try {
|
|
225
231
|
const command = await sandbox.process.getSessionCommand(
|
|
226
232
|
input.sessionId,
|
|
227
233
|
input.cmdId,
|
|
228
234
|
);
|
|
235
|
+
commandFound = Boolean(command);
|
|
229
236
|
exitCode =
|
|
230
237
|
typeof command?.exitCode === 'number' ? command.exitCode : null;
|
|
231
238
|
} catch (error) {
|
|
@@ -252,7 +259,9 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
252
259
|
stage: 'command_inspection_failed',
|
|
253
260
|
detail: commandInspectionError,
|
|
254
261
|
}
|
|
255
|
-
:
|
|
262
|
+
: commandFound && exitCode === null
|
|
263
|
+
? { stage: 'command_running', detail: null }
|
|
264
|
+
: { stage: 'result_missing', detail: null },
|
|
256
265
|
};
|
|
257
266
|
}
|
|
258
267
|
const result =
|
|
@@ -275,7 +284,13 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
275
284
|
return {
|
|
276
285
|
exitCode: null,
|
|
277
286
|
result: null,
|
|
278
|
-
diagnosis: {
|
|
287
|
+
diagnosis: {
|
|
288
|
+
stage:
|
|
289
|
+
failure.httpStatus === 404
|
|
290
|
+
? 'sandbox_missing'
|
|
291
|
+
: 'sandbox_lookup_failed',
|
|
292
|
+
detail: failure.detail,
|
|
293
|
+
},
|
|
279
294
|
};
|
|
280
295
|
}
|
|
281
296
|
}
|
|
@@ -874,6 +889,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
874
889
|
cmdId: start.cmdId,
|
|
875
890
|
runnerAttempt,
|
|
876
891
|
ceilingSeconds: DAYTONA_EXECUTE_TIMEOUT_SECONDS,
|
|
892
|
+
terminalGraceSeconds: PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
877
893
|
outputPath: stagedPayload.outputPath,
|
|
878
894
|
exitCodePath: stagedPayload.exitCodePath,
|
|
879
895
|
elapsedMs: Date.now() - startedAt,
|
|
@@ -890,7 +906,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
890
906
|
outputPath: stagedPayload.outputPath,
|
|
891
907
|
exitCodePath: stagedPayload.exitCodePath,
|
|
892
908
|
startedAtMs: Date.now(),
|
|
893
|
-
ceilingMs:
|
|
909
|
+
ceilingMs: DAYTONA_DETACHED_CEILING_SECONDS * 1_000,
|
|
894
910
|
},
|
|
895
911
|
logs: [],
|
|
896
912
|
stats: {},
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export const PLAY_RUNTIME_API_COMPAT_PATH = '/api/v2/plays/internal/runtime';
|
|
2
2
|
export const PLAY_RUNTIME_API_CURRENT_PATH = '/api/v2/internal/play-runtime';
|
|
3
|
+
export const PLAY_RUNTIME_OPERATION_ID_HEADER =
|
|
4
|
+
'x-deepline-runtime-operation-id';
|
|
5
|
+
export const PLAY_RUNTIME_OPERATION_ATTEMPT_HEADER =
|
|
6
|
+
'x-deepline-runtime-operation-attempt';
|
|
3
7
|
export const PLAY_RUNTIME_ATTEMPT_EXECUTOR_TOKEN_PATH =
|
|
4
8
|
'/api/v2/internal/play-runtime/executor-token';
|
|
5
9
|
export const PLAY_RUNTIME_TAIL_LOG_COMPAT_PATH =
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
export const STANDARD_PLAY_RUNTIME_LIMIT_SECONDS = 30 * 60;
|
|
3
3
|
export const STANDARD_PLAY_RUNTIME_LIMIT_LABEL = '30 minutes';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extra time after the user-code deadline for the sandbox runner to push its
|
|
7
|
+
* terminal, flush captured stdout, and let the crash epilogue finish.
|
|
8
|
+
*/
|
|
9
|
+
export const PLAY_RUNNER_TERMINAL_GRACE_SECONDS = 90;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling for Daytona runner startup before the customer runtime clock is
|
|
13
|
+
* armed. This covers the first durable heartbeat and the worker's reverse
|
|
14
|
+
* startup acknowledgement without charging either interval to user code.
|
|
15
|
+
*/
|
|
16
|
+
export const PLAY_RUNNER_STARTUP_GRACE_SECONDS = 4 * 60;
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* Runner timeout includes setup, cleanup, and billing headroom after the
|
|
7
20
|
* user-code runtime cap.
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export const PLAY_RUNTIME_CONTRACT = 3;
|
|
2
2
|
export const PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED = 1;
|
|
3
3
|
export const PLAY_RUNTIME_CONTRACT_HEADER = 'x-deepline-runtime-contract';
|
|
4
|
+
/**
|
|
5
|
+
* Opaque, per-request correlation key generated inside the sandbox runner.
|
|
6
|
+
* It contains no customer data and lets gateway logs distinguish a request that
|
|
7
|
+
* never reached Fly from one that was accepted but failed downstream.
|
|
8
|
+
*/
|
|
9
|
+
export const PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER =
|
|
10
|
+
'x-deepline-runtime-transport-attempt';
|
|
11
|
+
|
|
12
|
+
const RUNTIME_TRANSPORT_ATTEMPT_ID =
|
|
13
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate the opaque runner-generated correlation value before it appears in
|
|
17
|
+
* any gateway log or response header. It is intentionally not a credential.
|
|
18
|
+
*/
|
|
19
|
+
export function resolvePlayRuntimeTransportAttemptId(
|
|
20
|
+
raw: string | string[] | null | undefined,
|
|
21
|
+
): string | null {
|
|
22
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
23
|
+
const trimmed = value?.trim();
|
|
24
|
+
return trimmed && RUNTIME_TRANSPORT_ATTEMPT_ID.test(trimmed) ? trimmed : null;
|
|
25
|
+
}
|
|
4
26
|
|
|
5
27
|
export const RUNTIME_CONTRACT_CHANGELOG = [
|
|
6
28
|
{
|