deepline 0.1.252 → 0.1.253

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 (23) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +79 -64
  4. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +37 -35
  5. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +20 -5
  6. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +462 -29
  7. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +16 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +11 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +44 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +7 -4
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +98 -13
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +117 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +19 -3
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +1 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +13 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +22 -0
  17. package/dist/cli/index.js +26 -7
  18. package/dist/cli/index.mjs +26 -7
  19. package/dist/index.d.mts +2 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +6 -4
  22. package/dist/index.mjs +6 -4
  23. package/package.json +1 -1
@@ -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.split('\n').slice(0, PUBLIC_FAILURE_STACK_LINE_LIMIT).join('\n'),
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) : [];
@@ -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
- // A detached runner is bounded by both the play runtime ceiling and Daytona's
11
- // own inactivity stop. No background activity refresh keeps a wedged sandbox
12
- // alive past those limits.
13
- const DAYTONA_AUTO_STOP_INTERVAL_MINUTES = 30;
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 = 4 * 1024 * 1024;
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
- console.log('[crash-terminal] no push config; skipping');
221
+ writeDiagnostic('[crash-terminal] no push config; skipping');
174
222
  return;
175
223
  }
176
224
  const parsedResult = readResultFromOutput();
177
- const result = terminalResultForTransport(parsedResult || {
178
- status: 'failed',
179
- error:
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
- console.log('[crash-terminal] pushed status=' + result.status);
277
+ writeDiagnostic('[crash-terminal] pushed status=' + result.status);
213
278
  return;
214
279
  }
215
- console.log('[crash-terminal] rejected http=' + response.status);
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
- console.log(
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
- console.log('[crash-terminal] exhausted retries');
290
+ writeDiagnostic('[crash-terminal] exhausted retries');
226
291
  }
227
292
 
228
293
  main().catch((error) => {
229
- console.log('[crash-terminal] fatal ' + (error && error.message ? error.message : String(error)));
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}node ${shellQuote(runnerPath)} ${shellQuote(configPath)}`;
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
- const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(exitCodePath)} ${shellQuote(readyPath)} ${shellQuote(startAckPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} >> ${shellQuote(outputPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
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
- : { stage: 'result_missing', detail: null },
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: { stage: 'sandbox_lookup_failed', detail: failure.detail },
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: DAYTONA_EXECUTE_TIMEOUT_SECONDS * 1_000,
909
+ ceilingMs: DAYTONA_DETACHED_CEILING_SECONDS * 1_000,
894
910
  },
895
911
  logs: [],
896
912
  stats: {},
@@ -27,6 +27,7 @@ export type RuntimeResourceTerminalReason =
27
27
  | 'runner_failed'
28
28
  | 'sandbox_missing'
29
29
  | 'sandbox_oom'
30
+ | 'runtime_limit'
30
31
  | 'cancelled'
31
32
  | 'lease_expired';
32
33
 
@@ -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
  {
package/dist/cli/index.js CHANGED
@@ -635,7 +635,7 @@ var SDK_RELEASE = {
635
635
  // 0.1.252 hard-cuts the customer Monitor catalog to the two launched
636
636
  // Deepline-native radars. Older clients must update before discovering,
637
637
  // checking, or deploying an unlaunched monitor integration.
638
- version: "0.1.252",
638
+ version: "0.1.253",
639
639
  apiContract: "2026-07-native-monitor-launch-hard-cutover",
640
640
  supportPolicy: {
641
641
  minimumSupported: "0.1.53",
@@ -1482,12 +1482,14 @@ function createSecretRedactionContext(initialValues = []) {
1482
1482
  }
1483
1483
 
1484
1484
  // ../shared_libs/play-runtime/output-size-limits.ts
1485
- var encoder = typeof TextEncoder === "undefined" ? null : new TextEncoder();
1485
+ var TERMINAL_RUN_RESULT_MAX_BYTES = 6 * 1024 * 1024;
1486
1486
  var LEDGER_TERMINAL_RESULT_MAX_BYTES = 256 * 1024;
1487
- var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
1488
- var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
1487
+ var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 5 * 1024 * 1024;
1488
+ var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 5 * 1024 * 1024;
1489
1489
  var RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
1490
1490
  var RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
1491
+ var RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES = 16 * 1024 * 1024;
1492
+ var RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES = 64 * 1024;
1491
1493
 
1492
1494
  // ../shared_libs/play-runtime/ledger-safe-payload.ts
1493
1495
  var ledgerIngressRedactor = createSecretRedactionContext();
@@ -13330,15 +13332,24 @@ function collectDatasetHandleLines(value, path = "result") {
13330
13332
  return lines;
13331
13333
  }
13332
13334
  function buildRunWarnings(status, rowsInfo) {
13335
+ const result = readRecord(status.result);
13336
+ const metadata = readRecord(result?._metadata);
13337
+ const outputWarnings = Array.isArray(metadata?.outputWarnings) ? metadata.outputWarnings.map((warning) => readRecord(warning)).filter((warning) => {
13338
+ if (!warning) return false;
13339
+ return !(rowsInfo && !rowsInfo.complete && warning.reason === "array_preview_limit" && warning.originalItems === rowsInfo.totalRows && warning.retainedItems === rowsInfo.rows.length);
13340
+ }).map((warning) => warning?.message).filter(
13341
+ (message) => typeof message === "string" && message.trim().length > 0
13342
+ ).slice(0, 16) : [];
13333
13343
  if (status.status === "completed" && rowsInfo?.totalRows === 0) {
13334
- return ["Run completed with 0 output rows."];
13344
+ return ["Run completed with 0 output rows.", ...outputWarnings];
13335
13345
  }
13336
13346
  if (rowsInfo && !rowsInfo.complete) {
13337
13347
  return [
13338
- `Run output is partial: showing ${rowsInfo.rows.length} preview row(s) of ${rowsInfo.totalRows}.`
13348
+ `Run output is partial: showing ${rowsInfo.rows.length} preview row(s) of ${rowsInfo.totalRows}.`,
13349
+ ...outputWarnings
13339
13350
  ];
13340
13351
  }
13341
- return [];
13352
+ return outputWarnings;
13342
13353
  }
13343
13354
  function buildRunNextCommands(status) {
13344
13355
  const runId = status.runId?.trim();
@@ -14064,6 +14075,14 @@ function buildRunPackageTextLines(packaged) {
14064
14075
  if (failedLogWarning) {
14065
14076
  lines.push(` warning: ${failedLogWarning}`);
14066
14077
  }
14078
+ const packageWarnings = Array.isArray(packaged.warnings) ? Array.from(
14079
+ new Set(
14080
+ packaged.warnings.filter(
14081
+ (warning) => typeof warning === "string" && warning.trim().length > 0
14082
+ )
14083
+ )
14084
+ ).slice(0, 16) : [];
14085
+ lines.push(...packageWarnings.map((warning) => ` warning: ${warning}`));
14067
14086
  const failedLogNext = readRecord(failedLogs?.next);
14068
14087
  if (failedLogWarning && typeof failedLogNext?.logs === "string") {
14069
14088
  lines.push(