deepline 0.1.211 → 0.1.213

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 (54) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +317 -340
  2. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/client.ts +118 -3
  5. package/dist/bundling-sources/sdk/src/index.ts +2 -0
  6. package/dist/bundling-sources/sdk/src/play.ts +8 -1
  7. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -1
  9. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  10. package/dist/bundling-sources/sdk/src/types.ts +47 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +302 -30
  12. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
  14. package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +164 -121
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +42 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
  19. package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +163 -7
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
  23. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
  26. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
  30. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +91 -6
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
  34. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +540 -92
  35. package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
  37. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
  38. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
  39. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
  40. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
  42. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
  43. package/dist/cli/index.js +637 -229
  44. package/dist/cli/index.mjs +637 -229
  45. package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
  46. package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
  47. package/dist/index.d.mts +84 -11
  48. package/dist/index.d.ts +84 -11
  49. package/dist/index.js +225 -43
  50. package/dist/index.mjs +225 -43
  51. package/dist/plays/bundle-play-file.d.mts +2 -2
  52. package/dist/plays/bundle-play-file.d.ts +2 -2
  53. package/dist/plays/bundle-play-file.mjs +65 -4
  54. package/package.json +1 -1
@@ -30,7 +30,10 @@ export type RuntimeResourceSnapshot = {
30
30
  };
31
31
  sheetFlush: {
32
32
  acquired: number;
33
+ active: number;
34
+ waiting: number;
33
35
  bytes: number;
36
+ admissionWaitMsEwma: number | null;
34
37
  latencyMsEwma: number | null;
35
38
  };
36
39
  providers: Record<
@@ -90,8 +93,74 @@ function createEwma(alpha = 0.2): Ewma {
90
93
  };
91
94
  }
92
95
 
93
- function noOpLease(): RuntimeResourceLease {
94
- return { release() {} };
96
+ // Sheet persistence has its own local admission lane. Receipt lifecycle calls
97
+ // are admitted by the receipt sink/gateway and must not queue behind a large
98
+ // Runtime Sheet flush in the runner. Two flushes keep the database busy while
99
+ // bounding resident payloads and gateway/pool pressure (ADR 0012).
100
+ const MAX_CONCURRENT_SHEET_FLUSHES = 2;
101
+
102
+ type SheetFlushWaiter = {
103
+ resolve: (lease: RuntimeResourceLease) => void;
104
+ reject: (error: unknown) => void;
105
+ signal?: AbortSignal;
106
+ onAbort?: () => void;
107
+ };
108
+
109
+ class SheetFlushAdmission {
110
+ #active = 0;
111
+ #waiters: SheetFlushWaiter[] = [];
112
+
113
+ get active(): number {
114
+ return this.#active;
115
+ }
116
+
117
+ get waiting(): number {
118
+ return this.#waiters.length;
119
+ }
120
+
121
+ acquire(signal?: AbortSignal): Promise<RuntimeResourceLease> {
122
+ if (signal?.aborted) {
123
+ return Promise.reject(signal.reason ?? new Error('Sheet flush aborted'));
124
+ }
125
+ if (this.#active < MAX_CONCURRENT_SHEET_FLUSHES) {
126
+ this.#active += 1;
127
+ return Promise.resolve(this.#lease());
128
+ }
129
+ return new Promise((resolve, reject) => {
130
+ const waiter: SheetFlushWaiter = {
131
+ resolve,
132
+ reject,
133
+ ...(signal ? { signal } : {}),
134
+ };
135
+ if (signal) {
136
+ waiter.onAbort = () => {
137
+ const index = this.#waiters.indexOf(waiter);
138
+ if (index >= 0) this.#waiters.splice(index, 1);
139
+ reject(signal.reason ?? new Error('Sheet flush aborted'));
140
+ };
141
+ signal.addEventListener('abort', waiter.onAbort, { once: true });
142
+ }
143
+ this.#waiters.push(waiter);
144
+ });
145
+ }
146
+
147
+ #lease(): RuntimeResourceLease {
148
+ let released = false;
149
+ return {
150
+ release: () => {
151
+ if (released) return;
152
+ released = true;
153
+ while (this.#waiters.length > 0) {
154
+ const waiter = this.#waiters.shift()!;
155
+ waiter.signal?.removeEventListener('abort', waiter.onAbort!);
156
+ if (waiter.signal?.aborted) continue;
157
+ waiter.resolve(this.#lease());
158
+ return;
159
+ }
160
+ this.#active = Math.max(0, this.#active - 1);
161
+ },
162
+ };
163
+ }
95
164
  }
96
165
 
97
166
  function elapsedSince(startedAt: number): number {
@@ -109,6 +178,8 @@ export function createRuntimeResourceGovernor(input: {
109
178
  const rowAdmissionWait = createEwma();
110
179
  const toolAdmissionWait = createEwma();
111
180
  const sheetFlushLatency = createEwma();
181
+ const sheetFlushAdmissionWait = createEwma();
182
+ const sheetFlushAdmission = new SheetFlushAdmission();
112
183
  const providers = new Map<
113
184
  string,
114
185
  {
@@ -219,11 +290,14 @@ export function createRuntimeResourceGovernor(input: {
219
290
  },
220
291
 
221
292
  async acquireSheetFlush(sheetInput) {
293
+ const startedAt = Date.now();
294
+ const lease = await sheetFlushAdmission.acquire(sheetInput?.signal);
222
295
  sheetFlushCounters.acquired += 1;
296
+ sheetFlushAdmissionWait.observe(elapsedSince(startedAt));
223
297
  observe({
224
298
  sheetFlushBytes: sheetInput?.estimatedBytes ?? null,
225
299
  });
226
- return noOpLease();
300
+ return lease;
227
301
  },
228
302
 
229
303
  suggestedToolParallelism(toolId, fallback) {
@@ -258,7 +332,10 @@ export function createRuntimeResourceGovernor(input: {
258
332
  },
259
333
  sheetFlush: {
260
334
  acquired: sheetFlushCounters.acquired,
335
+ active: sheetFlushAdmission.active,
336
+ waiting: sheetFlushAdmission.waiting,
261
337
  bytes: sheetFlushCounters.bytes,
338
+ admissionWaitMsEwma: sheetFlushAdmissionWait.value,
262
339
  latencyMsEwma: sheetFlushLatency.value,
263
340
  },
264
341
  providers: Object.fromEntries(
@@ -7,6 +7,7 @@ import {
7
7
  nextPlayRunLedgerTerminalSource,
8
8
  normalizePlayRunLedgerTerminalSource,
9
9
  } from './run-terminal-source';
10
+ import { MAX_LEDGER_LOG_LINES_PER_EVENT } from './ledger-safe-payload';
10
11
 
11
12
  export type PlayRunLedgerStatus =
12
13
  | 'queued'
@@ -84,6 +85,18 @@ export type PlayRunLedgerStepSnapshot = {
84
85
  progress?: PlayRunLedgerStepProgress | null;
85
86
  };
86
87
 
88
+ export type PlayRunLedgerDatasetSnapshot = {
89
+ datasetId: string;
90
+ path: string;
91
+ tableNamespace: string;
92
+ phase: 'registered' | 'available' | 'failed';
93
+ persistedRows: number;
94
+ succeededRows: number;
95
+ failedRows: number;
96
+ complete: boolean;
97
+ updatedAt: number;
98
+ };
99
+
87
100
  export type PlayRunLedgerSnapshot = {
88
101
  runId: string;
89
102
  playName?: string | null;
@@ -96,6 +109,8 @@ export type PlayRunLedgerSnapshot = {
96
109
  durationMs?: number | null;
97
110
  orderedStepIds: string[];
98
111
  stepsById: Record<string, PlayRunLedgerStepSnapshot>;
112
+ orderedDatasetIds: string[];
113
+ datasetsById: Record<string, PlayRunLedgerDatasetSnapshot>;
99
114
  /**
100
115
  * Bounded tail of the run's log lines (last {@link LOG_TAIL_LIMIT}).
101
116
  * Full retention lives in the Run Log Stream (Convex `playRunLogChunks`);
@@ -217,10 +232,34 @@ export type PlayRunLedgerEvent =
217
232
  * repeated identical lines while absorbing redundant re-sends.
218
233
  */
219
234
  channelOffset?: number;
235
+ /** Scheduler attempt that owns this positional log channel. */
236
+ producerAttempt?: number;
237
+ /**
238
+ * Terminal transport replay normally matches ctx.log lines after
239
+ * removing timestamps. Detached-attempt recovery uses exact matching so
240
+ * the same message from a later attempt remains a distinct occurrence.
241
+ */
242
+ transportDedupe?: 'semantic' | 'exact';
220
243
  /** Set by ingestion when this append crossed the retention cap. */
221
244
  logsTruncated?: boolean;
222
245
  })
223
246
  | (PlayRunLedgerBaseEvent & {
247
+ type: 'dataset.lifecycle';
248
+ datasetId: string;
249
+ path: string;
250
+ tableNamespace: string;
251
+ phase: 'registered' | 'available' | 'failed';
252
+ persistedRows?: number;
253
+ succeededRows?: number;
254
+ failedRows?: number;
255
+ complete?: boolean;
256
+ })
257
+ | (PlayRunLedgerBaseEvent & {
258
+ /**
259
+ * @deprecated Mixed-version deploy seam only. Owner: Play Runtime.
260
+ * Remove after 2026-07-19, once pre-dataset-lifecycle workers have
261
+ * exceeded the maximum run duration plus the seven-day drain window.
262
+ */
224
263
  type: 'sheet.summary.updated';
225
264
  tableNamespace: string;
226
265
  deltaCursor?: number;
@@ -241,6 +280,7 @@ export type PlayRunLedgerStatusPatch = {
241
280
  * (see `slicePositionalLogLines`), forwarded onto the `log.appended` event.
242
281
  */
243
282
  liveLogsChannelOffset?: number | null;
283
+ liveLogsProducerAttempt?: number | null;
244
284
  liveNodeProgress?: unknown;
245
285
  result?: unknown;
246
286
  };
@@ -401,6 +441,8 @@ export function createEmptyPlayRunLedgerSnapshot(input: {
401
441
  : null,
402
442
  orderedStepIds: [],
403
443
  stepsById: {},
444
+ orderedDatasetIds: [],
445
+ datasetsById: {},
404
446
  logTail: [],
405
447
  totalLogCount: 0,
406
448
  logsTruncated: false,
@@ -447,6 +489,26 @@ export function normalizePlayRunLedgerSnapshot(
447
489
  progress: rawProgress ? normalizeStepProgress(rawProgress) : null,
448
490
  };
449
491
  }
492
+ const rawDatasets = isRecord(value.datasetsById) ? value.datasetsById : {};
493
+ const datasetsById: Record<string, PlayRunLedgerDatasetSnapshot> = {};
494
+ for (const [datasetId, rawDataset] of Object.entries(rawDatasets)) {
495
+ if (!datasetId.trim() || !isRecord(rawDataset)) continue;
496
+ const phase =
497
+ rawDataset.phase === 'available' || rawDataset.phase === 'failed'
498
+ ? rawDataset.phase
499
+ : 'registered';
500
+ datasetsById[datasetId] = {
501
+ datasetId,
502
+ path: optionalString(rawDataset.path) ?? `datasets.${datasetId}`,
503
+ tableNamespace: optionalString(rawDataset.tableNamespace) ?? datasetId,
504
+ phase,
505
+ persistedRows: Math.max(0, finiteNumber(rawDataset.persistedRows) ?? 0),
506
+ succeededRows: Math.max(0, finiteNumber(rawDataset.succeededRows) ?? 0),
507
+ failedRows: Math.max(0, finiteNumber(rawDataset.failedRows) ?? 0),
508
+ complete: rawDataset.complete === true,
509
+ updatedAt: finiteNumber(rawDataset.updatedAt) ?? 0,
510
+ };
511
+ }
450
512
  const createdAt = finiteNumber(value.createdAt) ?? fallback.createdAt ?? null;
451
513
  const startedAt = finiteNumber(value.startedAt) ?? fallback.startedAt ?? null;
452
514
  const finishedAt =
@@ -484,6 +546,14 @@ export function normalizePlayRunLedgerSnapshot(
484
546
  : finiteNumber(value.durationMs),
485
547
  orderedStepIds: orderedStepIds.filter((stepId) => stepsById[stepId]),
486
548
  stepsById,
549
+ orderedDatasetIds: (Array.isArray(value.orderedDatasetIds)
550
+ ? value.orderedDatasetIds
551
+ : Object.keys(datasetsById)
552
+ ).filter(
553
+ (datasetId): datasetId is string =>
554
+ typeof datasetId === 'string' && Boolean(datasetsById[datasetId]),
555
+ ),
556
+ datasetsById,
487
557
  logTail: logTail.slice(-LOG_TAIL_LIMIT),
488
558
  // Snapshots persisted before totalLogCount existed only know the retained
489
559
  // tail, so the best lower bound for the cumulative count is the tail size.
@@ -1269,6 +1339,34 @@ export function reducePlayRunLedgerEvent(
1269
1339
  : null,
1270
1340
  });
1271
1341
  }
1342
+ case 'dataset.lifecycle': {
1343
+ const current = base.datasetsById[event.datasetId];
1344
+ const next: PlayRunLedgerDatasetSnapshot = {
1345
+ datasetId: event.datasetId,
1346
+ path: event.path,
1347
+ tableNamespace: event.tableNamespace,
1348
+ phase: event.phase,
1349
+ persistedRows: Math.max(
1350
+ current?.persistedRows ?? 0,
1351
+ event.persistedRows ?? 0,
1352
+ ),
1353
+ succeededRows: Math.max(
1354
+ current?.succeededRows ?? 0,
1355
+ event.succeededRows ?? 0,
1356
+ ),
1357
+ failedRows: Math.max(current?.failedRows ?? 0, event.failedRows ?? 0),
1358
+ complete: current?.complete === true || event.complete === true,
1359
+ updatedAt: occurredAt,
1360
+ };
1361
+ return withTiming({
1362
+ ...base,
1363
+ orderedDatasetIds: current
1364
+ ? base.orderedDatasetIds
1365
+ : [...base.orderedDatasetIds, event.datasetId],
1366
+ datasetsById: { ...base.datasetsById, [event.datasetId]: next },
1367
+ resultTableNamespace: base.resultTableNamespace ?? event.tableNamespace,
1368
+ });
1369
+ }
1272
1370
  case 'sheet.summary.updated':
1273
1371
  return withTiming({
1274
1372
  ...base,
@@ -1376,6 +1474,8 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
1376
1474
  source?: PlayRunLedgerEventSource;
1377
1475
  occurredAt?: number;
1378
1476
  channelOffset?: number | null;
1477
+ producerAttempt?: number | null;
1478
+ transportDedupe?: 'semantic' | 'exact';
1379
1479
  includeLogAppend?: boolean;
1380
1480
  }): PlayRunLedgerEvent[] {
1381
1481
  const source = input.source ?? 'worker';
@@ -1392,16 +1492,31 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
1392
1492
  if (newLines.length === 0 || input.includeLogAppend === false) {
1393
1493
  return [];
1394
1494
  }
1395
- return [
1396
- {
1495
+ const events: PlayRunLedgerEvent[] = [];
1496
+ for (
1497
+ let index = 0;
1498
+ index < newLines.length;
1499
+ index += MAX_LEDGER_LOG_LINES_PER_EVENT
1500
+ ) {
1501
+ events.push({
1397
1502
  type: 'log.appended',
1398
1503
  runId: input.runId,
1399
1504
  source,
1400
1505
  occurredAt,
1401
- lines: newLines,
1402
- ...(positional ? { channelOffset: input.channelOffset! } : {}),
1403
- },
1404
- ];
1506
+ lines: newLines.slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT),
1507
+ ...(positional ? { channelOffset: input.channelOffset! + index } : {}),
1508
+ ...(positional &&
1509
+ typeof input.producerAttempt === 'number' &&
1510
+ Number.isFinite(input.producerAttempt) &&
1511
+ input.producerAttempt >= 0
1512
+ ? { producerAttempt: Math.trunc(input.producerAttempt) }
1513
+ : {}),
1514
+ ...(input.transportDedupe
1515
+ ? { transportDedupe: input.transportDedupe }
1516
+ : {}),
1517
+ });
1518
+ }
1519
+ return events;
1405
1520
  }
1406
1521
 
1407
1522
  /**
@@ -1419,6 +1534,8 @@ export function buildTerminalLogReplayEvents(input: {
1419
1534
  source?: PlayRunLedgerEventSource;
1420
1535
  occurredAt?: number;
1421
1536
  liveLogTotalCount?: number;
1537
+ dedupeMode?: 'semantic' | 'exact';
1538
+ producerAttempt?: number;
1422
1539
  }): PlayRunLedgerEvent[] {
1423
1540
  const channelOffset = terminalLogReplayChannelOffset({
1424
1541
  liveLogTotalCount: input.liveLogTotalCount,
@@ -1430,6 +1547,10 @@ export function buildTerminalLogReplayEvents(input: {
1430
1547
  source: 'coordinator',
1431
1548
  occurredAt: input.occurredAt,
1432
1549
  channelOffset,
1550
+ producerAttempt: input.producerAttempt,
1551
+ transportDedupe:
1552
+ input.dedupeMode ??
1553
+ (typeof input.producerAttempt === 'number' ? 'exact' : undefined),
1433
1554
  });
1434
1555
  }
1435
1556
 
@@ -1466,6 +1587,7 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
1466
1587
  source,
1467
1588
  occurredAt: checkpointAt,
1468
1589
  channelOffset: patch.liveLogsChannelOffset ?? null,
1590
+ producerAttempt: patch.liveLogsProducerAttempt ?? null,
1469
1591
  })
1470
1592
  : [];
1471
1593
  const earliestStartedAt =
@@ -4,6 +4,7 @@ import {
4
4
  normalizePlayRunLedgerSnapshot,
5
5
  summarizeRunRowOutcomes,
6
6
  type PlayRunLedgerSnapshot,
7
+ type PlayRunLedgerDatasetSnapshot,
7
8
  type PlayRunLedgerStepSnapshot,
8
9
  type PlayRunRowOutcomeSummary,
9
10
  } from './run-ledger';
@@ -76,6 +77,7 @@ export type PlayRunLiveSnapshot = {
76
77
  logsTruncated?: boolean;
77
78
  activeArtifactTableNamespace: string | null;
78
79
  resultTableNamespace: string | null;
80
+ datasets?: PlayRunLedgerDatasetSnapshot[];
79
81
  nodeStates: PlayRunStreamNodeState[];
80
82
  activeNodeId: string | null;
81
83
  /**
@@ -212,6 +214,11 @@ function buildSnapshotFromLedger(
212
214
  ...(snapshot.logsTruncated ? { logsTruncated: true } : {}),
213
215
  activeArtifactTableNamespace: snapshot.activeArtifactTableNamespace ?? null,
214
216
  resultTableNamespace: snapshot.resultTableNamespace ?? null,
217
+ datasets: snapshot.orderedDatasetIds
218
+ .map((datasetId) => snapshot.datasetsById[datasetId])
219
+ .filter((dataset): dataset is PlayRunLedgerDatasetSnapshot =>
220
+ Boolean(dataset),
221
+ ),
215
222
  nodeStates,
216
223
  activeNodeId: snapshot.activeStepId ?? null,
217
224
  ...(rowOutcomes ? { rowOutcomes } : {}),
@@ -1,5 +1,99 @@
1
1
  import type { DaytonaSandbox } from './daytona-lifecycle';
2
2
 
3
+ export type DetachedDaytonaStartupDiagnostic = {
4
+ commandFound: boolean;
5
+ exitCode: number | null;
6
+ sessionFound: boolean;
7
+ sessionCommandCount: number | null;
8
+ exitCodeFile: number | null;
9
+ processSummary: {
10
+ total: number;
11
+ node: number;
12
+ states: Record<string, number>;
13
+ } | null;
14
+ errors: string[];
15
+ };
16
+
17
+ function summarizeProcesses(value: unknown): {
18
+ total: number;
19
+ node: number;
20
+ states: Record<string, number>;
21
+ } | null {
22
+ if (typeof value !== 'string' || !value.trim()) return null;
23
+ const rows = value
24
+ .split('\n')
25
+ .map((line) => line.trim().split(/\s+/, 2))
26
+ .filter(([state, command]) => Boolean(state && command));
27
+ const states: Record<string, number> = {};
28
+ let node = 0;
29
+ for (const [state, command] of rows) {
30
+ const normalizedState = state![0]?.toUpperCase() || '?';
31
+ states[normalizedState] = (states[normalizedState] ?? 0) + 1;
32
+ if (/^node(?:-|$)/i.test(command!)) node += 1;
33
+ }
34
+ return { total: rows.length, node, states };
35
+ }
36
+
37
+ /**
38
+ * Inspect an accepted detached command without exposing its command line or
39
+ * environment. Daytona's command model has no queued/running state, so the
40
+ * combination of session membership, a numeric exit marker, and a
41
+ * aggregate process counts are the narrowest useful startup diagnostic.
42
+ * Process names are reduced to a Node count before logging; runner logs,
43
+ * output, and arbitrary process names are deliberately excluded because no
44
+ * heuristic redactor can guarantee removal of arbitrary customer secrets.
45
+ */
46
+ export async function inspectDetachedDaytonaStartup(input: {
47
+ sandbox: DaytonaSandbox;
48
+ sessionId: string;
49
+ cmdId: string;
50
+ exitCodePath: string;
51
+ }): Promise<DetachedDaytonaStartupDiagnostic> {
52
+ const errors: string[] = [];
53
+ const diagnostic: DetachedDaytonaStartupDiagnostic = {
54
+ commandFound: false,
55
+ exitCode: null,
56
+ sessionFound: false,
57
+ sessionCommandCount: null,
58
+ exitCodeFile: null,
59
+ processSummary: null,
60
+ errors,
61
+ };
62
+
63
+ await Promise.all([
64
+ input.sandbox.process
65
+ .getSessionCommand(input.sessionId, input.cmdId)
66
+ .then((command) => {
67
+ diagnostic.commandFound = true;
68
+ diagnostic.exitCode =
69
+ typeof command.exitCode === 'number' ? command.exitCode : null;
70
+ })
71
+ .catch(() => errors.push('get_session_command_failed')),
72
+ input.sandbox.process
73
+ .getSession(input.sessionId)
74
+ .then((session) => {
75
+ diagnostic.sessionFound = true;
76
+ diagnostic.sessionCommandCount = session.commands.length;
77
+ })
78
+ .catch(() => errors.push('get_session_failed')),
79
+ input.sandbox.fs
80
+ .downloadFile(input.exitCodePath, 5)
81
+ .then((file) => {
82
+ const value = Number.parseInt(file.toString('utf-8').trim(), 10);
83
+ diagnostic.exitCodeFile = Number.isFinite(value) ? value : null;
84
+ })
85
+ .catch(() => errors.push('exit_code_file_unavailable')),
86
+ input.sandbox.process
87
+ .executeCommand('ps -eo stat=,comm=', undefined, {}, 5)
88
+ .then((result) => {
89
+ diagnostic.processSummary = summarizeProcesses(result.result);
90
+ })
91
+ .catch(() => errors.push('process_snapshot_failed')),
92
+ ]);
93
+
94
+ return diagnostic;
95
+ }
96
+
3
97
  /**
4
98
  * Detached Daytona command start (push-execution B2-final).
5
99
  *
@@ -32,7 +32,10 @@ import {
32
32
  validateDaytonaExecutionContext,
33
33
  } from './daytona-lifecycle';
34
34
  import { stageDaytonaRunnerPayload } from './daytona-payload-transport';
35
- import { startDetachedDaytonaCommand } from './daytona-session-execution';
35
+ import {
36
+ inspectDetachedDaytonaStartup,
37
+ startDetachedDaytonaCommand,
38
+ } from './daytona-session-execution';
36
39
 
37
40
  const DAYTONA_EXECUTE_TIMEOUT_SECONDS = STANDARD_PLAY_RUNTIME_LIMIT_SECONDS;
38
41
  const STANDARD_WORKFLOW_RUNTIME_LIMIT_ERROR = `Based on this plan, max runtime is ${STANDARD_PLAY_RUNTIME_LIMIT_LABEL}. Use smaller batches; ask for runtime.`;
@@ -42,6 +45,8 @@ const DAYTONA_RUNTIME_INITIALIZATION_MAX_ATTEMPTS = 2;
42
45
  const DAYTONA_INFRASTRUCTURE_MAX_ATTEMPTS = 2;
43
46
  const DAYTONA_UPLOAD_MAX_ATTEMPTS = 2;
44
47
  const DAYTONA_UPLOAD_ATTEMPT_DEADLINE_MS = 90_000;
48
+ const DAYTONA_STARTUP_DIAGNOSTIC_DELAY_MS = 15_000;
49
+ const DAYTONA_STARTUP_TRACE_ENV = 'DEEPLINE_DAYTONA_STARTUP_TRACE';
45
50
 
46
51
  const RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN =
47
52
  /\bRuntime Postgres\b.*\b(connection timed out|connect timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|Connection terminated|Connection ended unexpectedly)\b/i;
@@ -122,19 +127,34 @@ export async function inspectDetachedDaytonaRunner(input: {
122
127
  cmdId: string;
123
128
  outputPath: string;
124
129
  exitCodePath: string;
125
- }): Promise<{ exitCode: number | null; result: PlayRunnerResult | null }> {
130
+ }): Promise<{
131
+ exitCode: number | null;
132
+ result: PlayRunnerResult | null;
133
+ diagnosis: {
134
+ stage:
135
+ | 'salvaged'
136
+ | 'result_missing'
137
+ | 'command_inspection_failed'
138
+ | 'sandbox_lookup_failed';
139
+ detail: string | null;
140
+ };
141
+ }> {
126
142
  try {
127
143
  const { clientOptions } = loadDaytonaRequiredConfig();
128
144
  const daytona = daytonaSdkClientFactory.createFull(clientOptions);
129
145
  const sandbox = (await daytona.get(input.sandboxId)) as DaytonaSandbox;
130
146
  let exitCode: number | null = null;
147
+ let commandInspectionError: string | null = null;
131
148
  try {
132
149
  const command = await sandbox.process.getSessionCommand(
133
150
  input.sessionId,
134
151
  input.cmdId,
135
152
  );
136
- exitCode = typeof command?.exitCode === 'number' ? command.exitCode : null;
153
+ exitCode =
154
+ typeof command?.exitCode === 'number' ? command.exitCode : null;
137
155
  } catch (error) {
156
+ commandInspectionError =
157
+ error instanceof Error ? error.message : String(error);
138
158
  console.warn('[play-runner.daytona.detached_inspect_command_failed]', {
139
159
  sandboxId: input.sandboxId,
140
160
  cmdId: input.cmdId,
@@ -148,7 +168,16 @@ export async function inspectDetachedDaytonaRunner(input: {
148
168
  timeoutMs: 10_000,
149
169
  });
150
170
  if (!recovered.execution) {
151
- return { exitCode, result: null };
171
+ return {
172
+ exitCode,
173
+ result: null,
174
+ diagnosis: commandInspectionError
175
+ ? {
176
+ stage: 'command_inspection_failed',
177
+ detail: commandInspectionError,
178
+ }
179
+ : { stage: 'result_missing', detail: null },
180
+ };
152
181
  }
153
182
  const result =
154
183
  findPlayRunnerResult(parsePlayRunnerEvents(recovered.execution.result)) ??
@@ -156,14 +185,22 @@ export async function inspectDetachedDaytonaRunner(input: {
156
185
  return {
157
186
  exitCode: exitCode ?? recovered.execution.exitCode,
158
187
  result,
188
+ diagnosis: result
189
+ ? { stage: 'salvaged', detail: null }
190
+ : { stage: 'result_missing', detail: null },
159
191
  };
160
192
  } catch (error) {
193
+ const detail = error instanceof Error ? error.message : String(error);
161
194
  console.warn('[play-runner.daytona.detached_inspect_failed]', {
162
195
  sandboxId: input.sandboxId,
163
196
  cmdId: input.cmdId,
164
- error: error instanceof Error ? error.message : String(error),
197
+ error: detail,
165
198
  });
166
- return { exitCode: null, result: null };
199
+ return {
200
+ exitCode: null,
201
+ result: null,
202
+ diagnosis: { stage: 'sandbox_lookup_failed', detail },
203
+ };
167
204
  }
168
205
  }
169
206
 
@@ -717,8 +754,56 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
717
754
  cmdId: start.cmdId,
718
755
  runnerAttempt,
719
756
  ceilingSeconds: DAYTONA_EXECUTE_TIMEOUT_SECONDS,
757
+ outputPath: stagedPayload.outputPath,
758
+ exitCodePath: stagedPayload.exitCodePath,
720
759
  elapsedMs: Date.now() - startedAt,
721
760
  });
761
+ // Daytona acknowledges an async command with only a cmdId. It does
762
+ // not expose a queued/running state, and the backend parks before the
763
+ // runner contacts the runtime sheet. During startup investigations,
764
+ // retain a bounded snapshot while the sandbox is still alive; the
765
+ // normal cancellation path otherwise deletes the only evidence.
766
+ // This is opt-in because it adds Daytona control-plane calls to every
767
+ // run. The diagnostic excludes command lines and environment values.
768
+ if (process.env[DAYTONA_STARTUP_TRACE_ENV] === '1') {
769
+ const diagnosticTimer = setTimeout(() => {
770
+ void inspectDetachedDaytonaStartup({
771
+ sandbox,
772
+ sessionId: start.sessionId,
773
+ cmdId: start.cmdId,
774
+ exitCodePath: stagedPayload.exitCodePath,
775
+ }).then(
776
+ (diagnostic) =>
777
+ emitDaytonaStage(
778
+ callbacks,
779
+ config.context,
780
+ 'execute:startup_diagnostic',
781
+ {
782
+ sandboxId: sandbox.id,
783
+ sessionId: start.sessionId,
784
+ cmdId: start.cmdId,
785
+ diagnostic,
786
+ elapsedMs: Date.now() - startedAt,
787
+ },
788
+ ),
789
+ (error: unknown) =>
790
+ emitDaytonaStage(
791
+ callbacks,
792
+ config.context,
793
+ 'execute:startup_diagnostic_failed',
794
+ {
795
+ sandboxId: sandbox.id,
796
+ sessionId: start.sessionId,
797
+ cmdId: start.cmdId,
798
+ errorType:
799
+ error instanceof Error ? error.name : 'unknown',
800
+ elapsedMs: Date.now() - startedAt,
801
+ },
802
+ ),
803
+ );
804
+ }, DAYTONA_STARTUP_DIAGNOSTIC_DELAY_MS);
805
+ diagnosticTimer.unref?.();
806
+ }
722
807
  return {
723
808
  status: 'suspended',
724
809
  suspension: {
@@ -9,16 +9,25 @@ import type {
9
9
  PlayRunnerResult,
10
10
  } from '@shared_libs/play-runtime/protocol';
11
11
 
12
- export type PlayRunnerRuntimeResource =
13
- | {
14
- kind: 'daytona_sandbox';
15
- sandboxId: string;
16
- billingStartedAt: number;
17
- billingEndedAt?: number | null;
18
- cpu?: number | null;
19
- memoryGiB?: number | null;
20
- diskGiB?: number | null;
21
- };
12
+ export type PlayRunnerRuntimeResource = {
13
+ kind: 'daytona_sandbox';
14
+ sandboxId: string;
15
+ billingStartedAt: number;
16
+ billingEndedAt?: number | null;
17
+ terminalReason?: RuntimeResourceTerminalReason | null;
18
+ terminalAt?: number | null;
19
+ cpu?: number | null;
20
+ memoryGiB?: number | null;
21
+ diskGiB?: number | null;
22
+ };
23
+
24
+ export type RuntimeResourceTerminalReason =
25
+ | 'completed'
26
+ | 'runner_failed'
27
+ | 'sandbox_missing'
28
+ | 'sandbox_oom'
29
+ | 'cancelled'
30
+ | 'lease_expired';
22
31
 
23
32
  export class RuntimeResourceFenceLostError extends Error {
24
33
  constructor(message: string) {