deepline 0.1.199 → 0.1.200

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 (72) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +5 -1
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +51 -17
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
  7. package/dist/bundling-sources/sdk/src/http.ts +5 -2
  8. package/dist/bundling-sources/sdk/src/play.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
  10. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  11. package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
  12. package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
  14. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
  15. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
  17. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
  18. package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
  22. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
  30. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
  31. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
  34. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +2 -3
  38. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +459 -103
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
  52. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
  53. package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
  55. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
  58. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
  59. package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
  60. package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
  61. package/dist/cli/index.js +167 -54
  62. package/dist/cli/index.mjs +167 -54
  63. package/dist/index.d.mts +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.js +8 -8
  66. package/dist/index.mjs +8 -8
  67. package/dist/plays/bundle-play-file.mjs +15 -20
  68. package/package.json +1 -1
  69. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
  70. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
  71. package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
  72. package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
@@ -0,0 +1,573 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { cp, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import type { SpawnOptionsWithoutStdio } from 'node:child_process';
6
+ import {
7
+ setSpanAttributes,
8
+ withActiveSpan,
9
+ } from '@shared_libs/observability/tracing';
10
+ import type { PlayRunnerBackend } from '../types';
11
+ import { buildPlayRunnerBundle } from '../bundle';
12
+ import {
13
+ findPlayRunnerResult,
14
+ parsePlayRunnerEventLine,
15
+ parsePlayRunnerEvents,
16
+ } from '../runner-events';
17
+ import type { PlayRunnerExecutionConfig } from '@shared_libs/play-runtime/protocol';
18
+ import { resolveDaytonaSandboxComputeItem } from '@shared_libs/play-runtime/worker-api-types';
19
+ import {
20
+ recordComputeBillingItemViaAppRuntime,
21
+ type WorkerRuntimeApiContext,
22
+ } from '@shared_libs/play-runtime/app-runtime-api';
23
+ import {
24
+ STANDARD_PLAY_RUNTIME_LIMIT_LABEL,
25
+ STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
26
+ } from '@shared_libs/play-runtime/runtime-constants';
27
+
28
+ interface CommandExecutionResult {
29
+ exitCode: number;
30
+ stdout: string;
31
+ stderr: string;
32
+ }
33
+
34
+ type StreamedCommandCallbacks = {
35
+ onStdoutLine?: (line: string) => void;
36
+ /** Called once when the result event is found — avoids storing full stdout. */
37
+ onResultLine?: (line: string) => void;
38
+ cancellationSignal?: AbortSignal;
39
+ };
40
+
41
+ const LOCAL_PROCESS_COMPUTE_SOURCE = 'local_process';
42
+ const LOCAL_PROCESS_COMPUTE_CPU = 1;
43
+ const LOCAL_PROCESS_COMPUTE_MEMORY_GIB = 1;
44
+ const LOCAL_PROCESS_COMPUTE_DISK_GIB = 3;
45
+ export const LOCAL_PROCESS_CANCEL_GRACE_MS = 2_000;
46
+ const LOCAL_PROCESS_CANCELLED_ERROR = 'Local play runner cancelled';
47
+ const STANDARD_PLAY_RUNTIME_LIMIT_ERROR = `Based on this plan, max runtime is ${STANDARD_PLAY_RUNTIME_LIMIT_LABEL}. Use smaller batches; ask for runtime.`;
48
+
49
+ export const LOCAL_PROCESS_COMPUTE_PROFILE = {
50
+ source: LOCAL_PROCESS_COMPUTE_SOURCE,
51
+ cpu: LOCAL_PROCESS_COMPUTE_CPU,
52
+ memoryGiB: LOCAL_PROCESS_COMPUTE_MEMORY_GIB,
53
+ diskGiB: LOCAL_PROCESS_COMPUTE_DISK_GIB,
54
+ } as const;
55
+
56
+ function logLocalRunnerPerf(
57
+ workflowId: string | null | undefined,
58
+ phase: string,
59
+ startedAt: number,
60
+ extra: Record<string, unknown> = {},
61
+ ) {
62
+ console.info('[perf][play-runner.local_process]', {
63
+ workflowId,
64
+ phase,
65
+ ms: Date.now() - startedAt,
66
+ ...extra,
67
+ });
68
+ }
69
+
70
+ async function runCommand(
71
+ command: string,
72
+ args: string[],
73
+ options?: SpawnOptionsWithoutStdio,
74
+ callbacks?: StreamedCommandCallbacks,
75
+ ): Promise<CommandExecutionResult> {
76
+ return await new Promise((resolve, reject) => {
77
+ if (callbacks?.cancellationSignal?.aborted) {
78
+ reject(new Error(LOCAL_PROCESS_CANCELLED_ERROR));
79
+ return;
80
+ }
81
+
82
+ const child = spawn(command, args, {
83
+ stdio: ['ignore', 'pipe', 'pipe'],
84
+ ...options,
85
+ });
86
+
87
+ let stderr = '';
88
+ let stdoutBuffer = '';
89
+ let stdoutLineCount = 0;
90
+ // Only keep the last line that looks like a result event (avoids storing full stdout).
91
+ let resultLine = '';
92
+ let settled = false;
93
+ let cancellationRequested = false;
94
+ let cancellationErrorMessage = LOCAL_PROCESS_CANCELLED_ERROR;
95
+ let cancelKillTimer: NodeJS.Timeout | null = null;
96
+
97
+ const cleanupCancellation = () => {
98
+ callbacks?.cancellationSignal?.removeEventListener('abort', onCancel);
99
+ if (cancelKillTimer) {
100
+ clearTimeout(cancelKillTimer);
101
+ cancelKillTimer = null;
102
+ }
103
+ };
104
+
105
+ const settleRejected = (error: unknown) => {
106
+ if (settled) {
107
+ return;
108
+ }
109
+ settled = true;
110
+ cleanupCancellation();
111
+ reject(error);
112
+ };
113
+
114
+ const settleResolved = (result: CommandExecutionResult) => {
115
+ if (settled) {
116
+ return;
117
+ }
118
+ settled = true;
119
+ cleanupCancellation();
120
+ resolve(result);
121
+ };
122
+
123
+ function onCancel() {
124
+ if (settled) {
125
+ return;
126
+ }
127
+ cancellationRequested = true;
128
+ const reason = callbacks?.cancellationSignal?.reason;
129
+ cancellationErrorMessage =
130
+ typeof reason === 'string' && reason.trim()
131
+ ? reason.trim()
132
+ : LOCAL_PROCESS_CANCELLED_ERROR;
133
+ child.kill('SIGTERM');
134
+ cancelKillTimer = setTimeout(() => {
135
+ if (child.exitCode === null && child.signalCode === null) {
136
+ child.kill('SIGKILL');
137
+ }
138
+ }, LOCAL_PROCESS_CANCEL_GRACE_MS);
139
+ cancelKillTimer.unref?.();
140
+ }
141
+
142
+ callbacks?.cancellationSignal?.addEventListener('abort', onCancel, {
143
+ once: true,
144
+ });
145
+
146
+ child.stdout?.on('data', (chunk) => {
147
+ const text = String(chunk);
148
+ stdoutBuffer += text;
149
+ const lines = stdoutBuffer.split(/\r?\n/);
150
+ stdoutBuffer = lines.pop() ?? '';
151
+ for (const line of lines) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed) {
154
+ continue;
155
+ }
156
+ stdoutLineCount++;
157
+ // Capture result lines so we can extract the result without storing all stdout.
158
+ if (trimmed.includes('"type":"result"')) {
159
+ resultLine = trimmed;
160
+ }
161
+ callbacks?.onStdoutLine?.(trimmed);
162
+ }
163
+ });
164
+ child.stdout?.on('error', () => {
165
+ // Absorb pipe errors — we still get partial stdout
166
+ });
167
+ child.stderr?.on('data', (chunk) => {
168
+ stderr += String(chunk);
169
+ });
170
+ child.stderr?.on('error', () => {});
171
+
172
+ child.on('error', settleRejected);
173
+ child.on('close', (code) => {
174
+ const trailing = stdoutBuffer.trim();
175
+ if (trailing) {
176
+ stdoutLineCount++;
177
+ if (trailing.includes('"type":"result"')) {
178
+ resultLine = trailing;
179
+ }
180
+ callbacks?.onStdoutLine?.(trailing);
181
+ }
182
+ if (cancellationRequested) {
183
+ settleRejected(new Error(cancellationErrorMessage));
184
+ return;
185
+ }
186
+ settleResolved({
187
+ exitCode: code ?? 0,
188
+ stdout: resultLine,
189
+ stderr,
190
+ });
191
+ });
192
+ });
193
+ }
194
+
195
+ async function runCommandWithStandardRuntimeLimit(
196
+ command: string,
197
+ args: string[],
198
+ options?: SpawnOptionsWithoutStdio,
199
+ callbacks?: StreamedCommandCallbacks,
200
+ ): Promise<CommandExecutionResult> {
201
+ const runtimeLimitController = new AbortController();
202
+ const runtimeDeadlineTimer = setTimeout(() => {
203
+ runtimeLimitController.abort(STANDARD_PLAY_RUNTIME_LIMIT_ERROR);
204
+ }, STANDARD_PLAY_RUNTIME_LIMIT_SECONDS * 1000);
205
+ runtimeDeadlineTimer.unref?.();
206
+
207
+ const onExternalCancel = () => {
208
+ if (!runtimeLimitController.signal.aborted) {
209
+ runtimeLimitController.abort(
210
+ callbacks?.cancellationSignal?.reason ?? LOCAL_PROCESS_CANCELLED_ERROR,
211
+ );
212
+ }
213
+ };
214
+
215
+ if (callbacks?.cancellationSignal?.aborted) {
216
+ onExternalCancel();
217
+ } else {
218
+ callbacks?.cancellationSignal?.addEventListener('abort', onExternalCancel, {
219
+ once: true,
220
+ });
221
+ }
222
+
223
+ try {
224
+ return await runCommand(command, args, options, {
225
+ ...callbacks,
226
+ cancellationSignal: runtimeLimitController.signal,
227
+ });
228
+ } finally {
229
+ clearTimeout(runtimeDeadlineTimer);
230
+ callbacks?.cancellationSignal?.removeEventListener(
231
+ 'abort',
232
+ onExternalCancel,
233
+ );
234
+ }
235
+ }
236
+
237
+ function createFailedResult(config: PlayRunnerExecutionConfig, error: string) {
238
+ return {
239
+ status: 'failed' as const,
240
+ error,
241
+ logs: [],
242
+ stats: {},
243
+ steps: [],
244
+ checkpoint: config.checkpoint ?? null,
245
+ tableNamespace: null,
246
+ };
247
+ }
248
+
249
+ function runtimeApiContextForConfig(
250
+ config: PlayRunnerExecutionConfig,
251
+ ): WorkerRuntimeApiContext {
252
+ const baseUrl = config.context.baseUrl?.trim();
253
+ const executorToken = config.context.executorToken?.trim();
254
+ if (!baseUrl || !executorToken) {
255
+ throw new Error(
256
+ 'Missing runtime API context for local-process compute billing.',
257
+ );
258
+ }
259
+ return { baseUrl, executorToken };
260
+ }
261
+
262
+ async function prepareLocalProcessWorkspace(
263
+ config: PlayRunnerExecutionConfig,
264
+ ): Promise<{
265
+ workspaceRoot: string;
266
+ runnerPath: string;
267
+ configPath: string;
268
+ localConfig: PlayRunnerExecutionConfig;
269
+ }> {
270
+ const workspaceRoot = await mkdtemp(
271
+ join(config.workspaceRoot, '.deepline-local-play-runner-'),
272
+ );
273
+ const filesRoot = join(workspaceRoot, 'files');
274
+ await mkdir(filesRoot, { recursive: true });
275
+
276
+ const localMaterializedFiles: Record<string, string> = {};
277
+ const copiedFiles = new Map<string, string>();
278
+
279
+ for (const [logicalPath, localPath] of Object.entries(
280
+ config.materializedFiles,
281
+ )) {
282
+ if (!copiedFiles.has(localPath)) {
283
+ const targetPath = join(filesRoot, randomUUID());
284
+ await mkdir(dirname(targetPath), { recursive: true });
285
+ await cp(localPath, targetPath, { force: true });
286
+ copiedFiles.set(localPath, targetPath);
287
+ }
288
+ localMaterializedFiles[logicalPath] = copiedFiles.get(localPath)!;
289
+ }
290
+
291
+ let localCsvSourcePath: string | null = null;
292
+ if (config.csvSourcePath) {
293
+ if (!copiedFiles.has(config.csvSourcePath)) {
294
+ const targetPath = join(filesRoot, randomUUID());
295
+ await mkdir(dirname(targetPath), { recursive: true });
296
+ await cp(config.csvSourcePath, targetPath, { force: true });
297
+ copiedFiles.set(config.csvSourcePath, targetPath);
298
+ }
299
+ localCsvSourcePath = copiedFiles.get(config.csvSourcePath)!;
300
+ }
301
+
302
+ const runnerPath = join(workspaceRoot, 'play-runner.cjs');
303
+ const configPath = join(workspaceRoot, 'runner-config.json');
304
+ const localConfig: PlayRunnerExecutionConfig = {
305
+ ...config,
306
+ workspaceRoot,
307
+ csvSourcePath: localCsvSourcePath,
308
+ materializedFiles: localMaterializedFiles,
309
+ };
310
+
311
+ return {
312
+ workspaceRoot,
313
+ runnerPath,
314
+ configPath,
315
+ localConfig,
316
+ };
317
+ }
318
+
319
+ export const localProcessPlayRunnerBackend: PlayRunnerBackend = {
320
+ async execute(config, callbacks) {
321
+ return await withActiveSpan(
322
+ 'plays.runner.local_process',
323
+ {
324
+ tracer: 'deepline.plays',
325
+ attributes: {
326
+ 'plays.workflow_id': config.context.workflowId,
327
+ 'plays.play_name': config.context.playName,
328
+ 'plays.has_csv_source': Boolean(config.csvSourcePath),
329
+ 'plays.materialized_file_count': Object.keys(config.materializedFiles)
330
+ .length,
331
+ },
332
+ },
333
+ async (backendSpan) => {
334
+ const localComputeStartedAt = Date.now();
335
+ let phaseStartedAt = Date.now();
336
+ const bundle = await withActiveSpan(
337
+ 'plays.runner.bundle_ready',
338
+ {
339
+ tracer: 'deepline.plays',
340
+ attributes: {
341
+ 'plays.workflow_id': config.context.workflowId,
342
+ 'plays.play_name': config.context.playName,
343
+ },
344
+ },
345
+ async () => await buildPlayRunnerBundle(),
346
+ );
347
+ logLocalRunnerPerf(
348
+ config.context.workflowId,
349
+ 'bundle_ready',
350
+ localComputeStartedAt,
351
+ { bundleBytes: bundle.length },
352
+ );
353
+ phaseStartedAt = Date.now();
354
+ const { workspaceRoot, runnerPath, configPath, localConfig } =
355
+ await withActiveSpan(
356
+ 'plays.runner.prepare_workspace',
357
+ {
358
+ tracer: 'deepline.plays',
359
+ attributes: {
360
+ 'plays.workflow_id': config.context.workflowId,
361
+ 'plays.play_name': config.context.playName,
362
+ },
363
+ },
364
+ async (span) => {
365
+ const prepared = await prepareLocalProcessWorkspace(config);
366
+ setSpanAttributes(span, {
367
+ 'plays.workspace_materialized_file_count': Object.keys(
368
+ prepared.localConfig.materializedFiles,
369
+ ).length,
370
+ 'plays.workspace_has_csv_source': Boolean(
371
+ prepared.localConfig.csvSourcePath,
372
+ ),
373
+ });
374
+ return prepared;
375
+ },
376
+ );
377
+ logLocalRunnerPerf(
378
+ config.context.workflowId,
379
+ 'prepare_workspace',
380
+ phaseStartedAt,
381
+ );
382
+
383
+ try {
384
+ phaseStartedAt = Date.now();
385
+ await withActiveSpan(
386
+ 'plays.runner.write_files',
387
+ {
388
+ tracer: 'deepline.plays',
389
+ attributes: {
390
+ 'plays.workflow_id': config.context.workflowId,
391
+ 'plays.play_name': config.context.playName,
392
+ },
393
+ },
394
+ async () => {
395
+ await writeFile(runnerPath, bundle, 'utf-8');
396
+ await writeFile(configPath, JSON.stringify(localConfig), 'utf-8');
397
+ },
398
+ );
399
+ logLocalRunnerPerf(
400
+ config.context.workflowId,
401
+ 'write_files',
402
+ phaseStartedAt,
403
+ );
404
+
405
+ phaseStartedAt = Date.now();
406
+ const execution = await withActiveSpan(
407
+ 'plays.runner.child_process',
408
+ {
409
+ tracer: 'deepline.plays',
410
+ attributes: {
411
+ 'plays.workflow_id': config.context.workflowId,
412
+ 'plays.play_name': config.context.playName,
413
+ },
414
+ },
415
+ async (span) => {
416
+ const result = await runCommandWithStandardRuntimeLimit(
417
+ 'node',
418
+ [runnerPath, configPath],
419
+ {
420
+ cwd: workspaceRoot,
421
+ env: {
422
+ ...process.env,
423
+ PWD: workspaceRoot,
424
+ TMPDIR: workspaceRoot,
425
+ TEMP: workspaceRoot,
426
+ TMP: workspaceRoot,
427
+ },
428
+ },
429
+ {
430
+ cancellationSignal: callbacks?.cancellationSignal,
431
+ onStdoutLine: (line) => {
432
+ const event = parsePlayRunnerEventLine(line);
433
+ if (!event) {
434
+ return;
435
+ }
436
+ if (event.type === 'log') {
437
+ callbacks?.onLog?.(event);
438
+ return;
439
+ }
440
+ if (event.type === 'checkpoint') {
441
+ callbacks?.onCheckpoint?.(event.checkpoint);
442
+ return;
443
+ }
444
+ if (event.type === 'row_update') {
445
+ callbacks?.onRowUpdate?.(event.update);
446
+ return;
447
+ }
448
+ if (event.type === 'execution_event') {
449
+ callbacks?.onExecutionEvent?.(event.event);
450
+ }
451
+ },
452
+ },
453
+ );
454
+ setSpanAttributes(span, {
455
+ 'plays.child_exit_code': result.exitCode,
456
+ 'plays.stdout_bytes': result.stdout.length,
457
+ 'plays.stderr_bytes': result.stderr.length,
458
+ });
459
+ return result;
460
+ },
461
+ );
462
+ logLocalRunnerPerf(
463
+ config.context.workflowId,
464
+ 'child_process',
465
+ phaseStartedAt,
466
+ {
467
+ exitCode: execution.exitCode,
468
+ stdoutBytes: execution.stdout.length,
469
+ stderrBytes: execution.stderr.length,
470
+ },
471
+ );
472
+
473
+ phaseStartedAt = Date.now();
474
+ const events = await withActiveSpan(
475
+ 'plays.runner.parse_events',
476
+ {
477
+ tracer: 'deepline.plays',
478
+ attributes: {
479
+ 'plays.workflow_id': config.context.workflowId,
480
+ 'plays.play_name': config.context.playName,
481
+ },
482
+ },
483
+ async (span) => {
484
+ const parsed = parsePlayRunnerEvents(execution.stdout);
485
+ setSpanAttributes(span, {
486
+ 'plays.event_count': parsed.length,
487
+ });
488
+ return parsed;
489
+ },
490
+ );
491
+ logLocalRunnerPerf(
492
+ config.context.workflowId,
493
+ 'parse_events',
494
+ phaseStartedAt,
495
+ { eventCount: events.length },
496
+ );
497
+ const result = findPlayRunnerResult(events);
498
+ if (result) {
499
+ setSpanAttributes(backendSpan, {
500
+ 'plays.runner_status': result.status,
501
+ });
502
+ return result;
503
+ }
504
+
505
+ return createFailedResult(
506
+ config,
507
+ execution.exitCode !== 0
508
+ ? `Local play runner exited with code ${execution.exitCode}.${execution.stderr ? ` ${execution.stderr.trim()}` : ''}`.trim()
509
+ : 'Local play runner produced no result.',
510
+ );
511
+ } catch (error) {
512
+ return createFailedResult(
513
+ config,
514
+ error instanceof Error ? error.message : String(error),
515
+ );
516
+ } finally {
517
+ const localComputeEndedAt = Date.now();
518
+ try {
519
+ await recordComputeBillingItemViaAppRuntime(
520
+ runtimeApiContextForConfig(config),
521
+ {
522
+ sessionId:
523
+ config.context.workflowId ??
524
+ config.context.runId ??
525
+ workspaceRoot,
526
+ orgId: config.context.orgId ?? 'unknown',
527
+ userId: null,
528
+ operation: 'workflow_run',
529
+ item: resolveDaytonaSandboxComputeItem({
530
+ itemId: `${LOCAL_PROCESS_COMPUTE_SOURCE}:${config.context.workflowId ?? workspaceRoot}`,
531
+ source: LOCAL_PROCESS_COMPUTE_SOURCE,
532
+ sandboxId: workspaceRoot,
533
+ wallTimeSeconds:
534
+ (localComputeEndedAt - localComputeStartedAt) / 1000,
535
+ cpu: LOCAL_PROCESS_COMPUTE_PROFILE.cpu,
536
+ memoryGiB: LOCAL_PROCESS_COMPUTE_PROFILE.memoryGiB,
537
+ diskGiB: LOCAL_PROCESS_COMPUTE_PROFILE.diskGiB,
538
+ startedAt: localComputeStartedAt,
539
+ endedAt: localComputeEndedAt,
540
+ }),
541
+ },
542
+ );
543
+ } catch (billingError) {
544
+ console.error(
545
+ '[play-runner.local_process.compute_billing_failed]',
546
+ {
547
+ workflowId: config.context.workflowId,
548
+ workspaceRoot,
549
+ error:
550
+ billingError instanceof Error
551
+ ? billingError.message
552
+ : String(billingError),
553
+ },
554
+ );
555
+ }
556
+ await withActiveSpan(
557
+ 'plays.runner.cleanup',
558
+ {
559
+ tracer: 'deepline.plays',
560
+ attributes: {
561
+ 'plays.workflow_id': config.context.workflowId,
562
+ 'plays.play_name': config.context.playName,
563
+ },
564
+ },
565
+ async () => {
566
+ await rm(workspaceRoot, { recursive: true, force: true });
567
+ },
568
+ );
569
+ }
570
+ },
571
+ );
572
+ },
573
+ };