deepline 0.1.12 → 0.1.20

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 (82) hide show
  1. package/README.md +14 -6
  2. package/dist/cli/index.js +1346 -717
  3. package/dist/cli/index.mjs +1342 -713
  4. package/dist/index.d.mts +199 -23
  5. package/dist/index.d.ts +199 -23
  6. package/dist/index.js +221 -14
  7. package/dist/index.mjs +221 -14
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +214 -77
  9. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
  10. package/dist/repo/apps/play-runner-workers/src/entry.ts +385 -66
  11. package/dist/repo/sdk/src/client.ts +237 -0
  12. package/dist/repo/sdk/src/config.ts +125 -8
  13. package/dist/repo/sdk/src/http.ts +29 -5
  14. package/dist/repo/sdk/src/play.ts +19 -36
  15. package/dist/repo/sdk/src/plays/bundle-play-file.ts +22 -8
  16. package/dist/repo/sdk/src/plays/local-file-discovery.ts +207 -160
  17. package/dist/repo/sdk/src/types.ts +25 -0
  18. package/dist/repo/sdk/src/version.ts +2 -2
  19. package/dist/repo/shared_libs/play-runtime/tool-result.ts +237 -145
  20. package/dist/repo/shared_libs/plays/bundling/index.ts +206 -229
  21. package/dist/repo/shared_libs/plays/dataset.ts +28 -0
  22. package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
  23. package/package.json +5 -4
  24. package/dist/cli/index.js.map +0 -1
  25. package/dist/cli/index.mjs.map +0 -1
  26. package/dist/index.js.map +0 -1
  27. package/dist/index.mjs.map +0 -1
  28. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +0 -21
  29. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +0 -177
  30. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +0 -52
  31. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +0 -100
  32. package/dist/repo/sdk/src/cli/commands/auth.ts +0 -500
  33. package/dist/repo/sdk/src/cli/commands/billing.ts +0 -188
  34. package/dist/repo/sdk/src/cli/commands/csv.ts +0 -123
  35. package/dist/repo/sdk/src/cli/commands/db.ts +0 -119
  36. package/dist/repo/sdk/src/cli/commands/feedback.ts +0 -40
  37. package/dist/repo/sdk/src/cli/commands/org.ts +0 -117
  38. package/dist/repo/sdk/src/cli/commands/play.ts +0 -3441
  39. package/dist/repo/sdk/src/cli/commands/tools.ts +0 -687
  40. package/dist/repo/sdk/src/cli/dataset-stats.ts +0 -415
  41. package/dist/repo/sdk/src/cli/index.ts +0 -148
  42. package/dist/repo/sdk/src/cli/progress.ts +0 -149
  43. package/dist/repo/sdk/src/cli/skills-sync.ts +0 -141
  44. package/dist/repo/sdk/src/cli/trace.ts +0 -61
  45. package/dist/repo/sdk/src/cli/utils.ts +0 -145
  46. package/dist/repo/sdk/src/compat.ts +0 -77
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +0 -129
  48. package/dist/repo/shared_libs/observability/tracing.ts +0 -98
  49. package/dist/repo/shared_libs/play-runtime/context.ts +0 -4242
  50. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +0 -250
  51. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +0 -725
  52. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +0 -10
  53. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +0 -304
  54. package/dist/repo/shared_libs/play-runtime/db-session.ts +0 -462
  55. package/dist/repo/shared_libs/play-runtime/live-events.ts +0 -214
  56. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +0 -50
  57. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +0 -114
  58. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +0 -158
  59. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +0 -172
  60. package/dist/repo/shared_libs/play-runtime/protocol.ts +0 -121
  61. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +0 -42
  62. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +0 -33
  63. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +0 -1873
  64. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +0 -2
  65. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +0 -201
  66. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +0 -48
  67. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +0 -84
  68. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +0 -147
  69. package/dist/repo/shared_libs/play-runtime/suspension.ts +0 -68
  70. package/dist/repo/shared_libs/play-runtime/tracing.ts +0 -31
  71. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +0 -75
  72. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +0 -140
  73. package/dist/repo/shared_libs/plays/artifact-transport.ts +0 -14
  74. package/dist/repo/shared_libs/plays/artifact-types.ts +0 -49
  75. package/dist/repo/shared_libs/plays/compiler-manifest.ts +0 -186
  76. package/dist/repo/shared_libs/plays/definition.ts +0 -264
  77. package/dist/repo/shared_libs/plays/file-refs.ts +0 -11
  78. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +0 -206
  79. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +0 -164
  80. package/dist/repo/shared_libs/plays/runtime-validation.ts +0 -395
  81. package/dist/repo/shared_libs/temporal/constants.ts +0 -39
  82. package/dist/repo/shared_libs/temporal/preview-config.ts +0 -153
@@ -63,6 +63,7 @@ import {
63
63
  } from '../../../shared_libs/plays/row-identity';
64
64
  import {
65
65
  getCompiledPipelineSubsteps,
66
+ flattenStaticPipeline,
66
67
  resolveSheetContractForTableNamespace,
67
68
  sqlSafePlayColumnName,
68
69
  type PlayStaticPipeline,
@@ -204,11 +205,18 @@ type WorkerEnv = {
204
205
  * loud error. Loud failures > silent fallbacks.
205
206
  */
206
207
  HARNESS?: import('../../play-harness-worker/src/rpc-types').PlayHarnessRpc;
208
+ VERCEL_PROTECTION_BYPASS_TOKEN?: string;
207
209
  };
208
210
 
209
211
  let cachedRuntimeApiBinding: WorkerEnv['RUNTIME_API'] | null = null;
212
+ let cachedRuntimeApiVercelBypassToken: string | null = null;
210
213
  function captureRuntimeApiBinding(env: WorkerEnv): void {
211
214
  cachedRuntimeApiBinding = env.RUNTIME_API ?? null;
215
+ cachedRuntimeApiVercelBypassToken =
216
+ typeof env.VERCEL_PROTECTION_BYPASS_TOKEN === 'string' &&
217
+ env.VERCEL_PROTECTION_BYPASS_TOKEN.trim()
218
+ ? env.VERCEL_PROTECTION_BYPASS_TOKEN.trim()
219
+ : null;
212
220
  }
213
221
 
214
222
  let cachedCoordinatorBinding: WorkerEnv['COORDINATOR'] | null = null;
@@ -288,22 +296,20 @@ async function probeHarnessOnce(
288
296
  }
289
297
  }
290
298
  /**
291
- * Routes runtime API requests through the in-process RUNTIME_API binding.
292
- * The coordinator MUST provide this binding there is no public-fetch fallback.
299
+ * Routes runtime API requests through the in-process RUNTIME_API binding when
300
+ * Cloudflare exposes the coordinator WorkerEntrypoint export. Some workflow
301
+ * execution paths do not expose those exports; there we keep the older public
302
+ * fetch transport so the play still reaches the same authenticated handler.
293
303
  */
294
304
  const RUNTIME_API_TIMEOUT_MS = 30_000;
295
305
  const RUNTIME_API_PLAY_RUN_TIMEOUT_MS = 75_000;
306
+ let loggedMissingRuntimeApiBinding = false;
296
307
 
297
308
  async function fetchRuntimeApi(
298
309
  baseUrl: string,
299
310
  path: string,
300
311
  init: RequestInit,
301
312
  ): Promise<Response> {
302
- if (!cachedRuntimeApiBinding) {
303
- throw new Error(
304
- `[play-harness] RUNTIME_API binding missing — coordinator did not wire it before invoking the play. path=${path}`,
305
- );
306
- }
307
313
  const timeoutMs =
308
314
  path === '/api/v2/plays/run'
309
315
  ? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
@@ -313,16 +319,25 @@ async function fetchRuntimeApi(
313
319
  try {
314
320
  const mergedInit: RequestInit = {
315
321
  ...init,
322
+ headers: runtimeApiHeaders(init.headers, cachedRuntimeApiBinding == null),
316
323
  signal: controller.signal,
317
324
  };
318
- const res = await cachedRuntimeApiBinding.fetch(
319
- new Request(`${baseUrl}${path}`, mergedInit),
325
+ if (!cachedRuntimeApiBinding) {
326
+ if (!loggedMissingRuntimeApiBinding) {
327
+ loggedMissingRuntimeApiBinding = true;
328
+ console.warn(
329
+ `[play-harness] RUNTIME_API binding missing; using public runtime API transport. path=${path}`,
330
+ );
331
+ }
332
+ return await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, mergedInit);
333
+ }
334
+ return await cachedRuntimeApiBinding.fetch(
335
+ new Request(`${baseUrl.replace(/\/$/, '')}${path}`, mergedInit),
320
336
  );
321
- return res;
322
337
  } catch (err) {
323
338
  if (err instanceof Error && err.name === 'AbortError') {
324
339
  throw new Error(
325
- `[play-harness] RUNTIME_API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
340
+ `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
326
341
  );
327
342
  }
328
343
  throw err;
@@ -331,10 +346,29 @@ async function fetchRuntimeApi(
331
346
  }
332
347
  }
333
348
 
349
+ function runtimeApiHeaders(
350
+ headers: HeadersInit | undefined,
351
+ includeVercelBypass: boolean,
352
+ ): Headers {
353
+ const next = new Headers(headers);
354
+ if (includeVercelBypass) {
355
+ const bypassToken = cachedVercelProtectionBypassToken();
356
+ if (bypassToken) {
357
+ next.set('x-vercel-protection-bypass', bypassToken);
358
+ }
359
+ }
360
+ return next;
361
+ }
362
+
363
+ function cachedVercelProtectionBypassToken(): string | null {
364
+ return cachedRuntimeApiVercelBypassToken;
365
+ }
366
+
334
367
  const WORKER_PLAY_CALL_LIMITS = {
335
368
  maxPlayCallDepth: 6,
336
- maxPlayCallCount: 32,
337
- maxChildPlayCallsPerParent: 16,
369
+ maxPlayCallCount: 1_000,
370
+ maxChildPlayCallsPerParent: 1_000,
371
+ maxConcurrentPlayCalls: 16,
338
372
  };
339
373
 
340
374
  /**
@@ -350,6 +384,20 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
350
384
  count?: number;
351
385
  datasetKind?: 'csv' | 'map';
352
386
  cacheSummary?: string | null;
387
+ workProgress?: {
388
+ total: number;
389
+ executed: number;
390
+ reused: number;
391
+ skipped: number;
392
+ pending: number;
393
+ failed: number;
394
+ degraded?: boolean;
395
+ duplicates?: {
396
+ exact?: number;
397
+ semantic?: number;
398
+ rejected?: number;
399
+ };
400
+ };
353
401
  },
354
402
  ): T[] & {
355
403
  count(): Promise<number>;
@@ -363,6 +411,7 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
363
411
  const count = Math.max(0, Math.floor(options?.count ?? rows.length));
364
412
  const datasetKind = options?.datasetKind ?? 'map';
365
413
  const cacheSummary = options?.cacheSummary ?? null;
414
+ const workProgress = options?.workProgress;
366
415
  // Build the array result. JSON.stringify on arrays calls toJSON only if
367
416
  // present on the array itself — we attach below. The dataset metadata is
368
417
  // also exposed via own properties so plays can `enriched.count()` etc.
@@ -415,6 +464,10 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
415
464
  value: cacheSummary,
416
465
  enumerable: false,
417
466
  });
467
+ Object.defineProperty(arr, '__deeplineWorkProgress', {
468
+ value: workProgress,
469
+ enumerable: false,
470
+ });
418
471
  // Plays often `return { rows: dataset, count: N }`. JSON.stringify on the
419
472
  // array would normally produce `[row, row, ...]` — we want the dataset
420
473
  // envelope shape instead so assertions seeing `result.rows.columns` pass.
@@ -435,6 +488,9 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
435
488
  preview: plainRows,
436
489
  tableNamespace: name,
437
490
  ...(cacheSummary ? { cacheSummary } : {}),
491
+ ...(workProgress
492
+ ? { _metadata: { workProgress } }
493
+ : {}),
438
494
  };
439
495
  },
440
496
  enumerable: false,
@@ -454,6 +510,7 @@ type RunnerEvent =
454
510
  | { type: 'error'; message: string; stack?: string; ts: number };
455
511
 
456
512
  type WorkflowRunOutput = {
513
+ playName: string;
457
514
  result: unknown;
458
515
  outputRows: number;
459
516
  durationMs: number;
@@ -469,7 +526,12 @@ function makeRequestId(): string {
469
526
  }
470
527
 
471
528
  function publicCsvInputRow<T extends Record<string, unknown>>(row: T): T {
472
- return stripCsvProjectedFields(row) as T;
529
+ const stripped = stripCsvProjectedFields(row) as Record<string, unknown>;
530
+ return Object.fromEntries(
531
+ Object.entries(stripped).filter(
532
+ ([fieldName]) => !fieldName.startsWith('__deepline'),
533
+ ),
534
+ ) as T;
473
535
  }
474
536
 
475
537
  /**
@@ -577,6 +639,7 @@ async function postRuntimeApiBestEffort(
577
639
  async function submitChildPlayThroughCoordinator(input: {
578
640
  req: RunRequest;
579
641
  body: unknown;
642
+ allowInline?: boolean;
580
643
  }): Promise<{
581
644
  workflowId?: string;
582
645
  runId?: string;
@@ -588,7 +651,7 @@ async function submitChildPlayThroughCoordinator(input: {
588
651
  logs?: string[];
589
652
  timings?: Array<{ phase: string; ms: number }>;
590
653
  }> {
591
- if (cachedCoordinatorBinding) {
654
+ if (cachedCoordinatorBinding && input.allowInline !== false) {
592
655
  if (!isRecord(input.body)) {
593
656
  throw new Error('ctx.runPlay child submit requires an object body.');
594
657
  }
@@ -714,6 +777,11 @@ function childPlayEventKey(input: { key: string; workflowId: string }): string {
714
777
  return `child_play_${hashChildPlayEventKey(`${input.key}:${input.workflowId}`)}_${readableKey}`;
715
778
  }
716
779
 
780
+ function workflowTimeoutFromMs(timeoutMs: number): string {
781
+ const seconds = Math.max(1, Math.ceil(timeoutMs / 1000));
782
+ return `${seconds} second${seconds === 1 ? '' : 's'}`;
783
+ }
784
+
717
785
  async function waitForChildPlayTerminalEvent(input: {
718
786
  req: RunRequest;
719
787
  workflowStep?: WorkflowStep;
@@ -734,11 +802,11 @@ async function waitForChildPlayTerminalEvent(input: {
734
802
  const event = (await (
735
803
  input.workflowStep.waitForEvent as unknown as (
736
804
  name: string,
737
- options: { type: string; timeout: number },
805
+ options: { type: string; timeout: string },
738
806
  ) => Promise<{ payload: unknown }>
739
807
  )(`child_play_terminal:${eventKey}`, {
740
808
  type: integrationEventType(eventKey),
741
- timeout: input.timeoutMs,
809
+ timeout: workflowTimeoutFromMs(input.timeoutMs),
742
810
  })) as { payload: unknown };
743
811
  const rawPayload = isRecord(event.payload) ? event.payload : {};
744
812
  const payload = isRecord(rawPayload.data) ? rawPayload.data : rawPayload;
@@ -909,15 +977,25 @@ async function waitForSyntheticIntegrationEvent(
909
977
  typeof input.timeout_ms === 'number' && Number.isFinite(input.timeout_ms)
910
978
  ? Math.max(1, Math.round(input.timeout_ms))
911
979
  : 30_000;
980
+ await postRuntimeApiBestEffort(req.baseUrl, req.executorToken, {
981
+ action: 'update_run_status',
982
+ playId: req.runId,
983
+ status: 'running',
984
+ runtimeBackend: 'cf_workflows_dynamic_worker',
985
+ waitKind: 'integration_event_batch',
986
+ waitUntil: nowMs() + timeoutMs,
987
+ activeBoundaryId: `integration_event:${eventKey}`,
988
+ lastCheckpointAt: nowMs(),
989
+ });
912
990
  try {
913
991
  const event = (await (
914
992
  workflowStep.waitForEvent as unknown as (
915
993
  name: string,
916
- options: { type: string; timeout: number },
994
+ options: { type: string; timeout: string },
917
995
  ) => Promise<{ payload: unknown }>
918
996
  )(`integration_event:${eventKey}`, {
919
997
  type: integrationEventType(eventKey),
920
- timeout: timeoutMs,
998
+ timeout: workflowTimeoutFromMs(timeoutMs),
921
999
  })) as { payload: unknown };
922
1000
  const payload =
923
1001
  event.payload &&
@@ -983,7 +1061,7 @@ async function callToolDirect(
983
1061
  headers: {
984
1062
  'content-type': 'application/json',
985
1063
  authorization: `Bearer ${req.executorToken}`,
986
- 'x-deepline-request-id': `${req.runId}:${toolId}:${id}`,
1064
+ 'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${attempt}`,
987
1065
  [EXECUTE_TOOL_METADATA_HEADER]: 'true',
988
1066
  },
989
1067
  body: JSON.stringify({ payload: input }),
@@ -1455,7 +1533,9 @@ async function executeBatchedWorkerToolGroup(input: {
1455
1533
  ) => {
1456
1534
  for (const entry of chunkResults) {
1457
1535
  const batchResult = isToolExecuteResult(entry.result)
1458
- ? entry.result.result.data
1536
+ ? isRecordLike(entry.result.result)
1537
+ ? entry.result.result.data
1538
+ : undefined
1459
1539
  : entry.result;
1460
1540
  const splitResults =
1461
1541
  batchResult != null
@@ -1516,6 +1596,7 @@ type WorkerMapChunkSummary<T extends Record<string, unknown>> = {
1516
1596
  rowsWritten: number;
1517
1597
  rowsExecuted: number;
1518
1598
  rowsCached: number;
1599
+ rowsDuplicateReused: number;
1519
1600
  rowsInserted: number;
1520
1601
  rowsSkipped: number;
1521
1602
  outputDatasetId: string;
@@ -1641,22 +1722,32 @@ async function executeWorkerStepProgram(
1641
1722
  path: string[];
1642
1723
  outputs: RecordedStepProgramOutput[];
1643
1724
  },
1725
+ workflowStep?: WorkflowStep,
1644
1726
  ): Promise<unknown> {
1645
1727
  let currentRow: Record<string, unknown> = cloneCsvAliasedRow(inputRow);
1646
1728
  for (const step of program.steps) {
1647
1729
  const stepPath = [...(recorder?.path ?? []), step.name];
1648
- const resolution = await executeWorkerStepResolver(
1649
- step.resolver,
1650
- currentRow,
1651
- ctx,
1652
- index,
1653
- recorder
1654
- ? {
1655
- ...recorder,
1656
- path: stepPath,
1657
- }
1658
- : undefined,
1659
- );
1730
+ const runStep = async () =>
1731
+ await executeWorkerStepResolver(
1732
+ step.resolver,
1733
+ currentRow,
1734
+ ctx,
1735
+ index,
1736
+ recorder
1737
+ ? {
1738
+ ...recorder,
1739
+ path: stepPath,
1740
+ }
1741
+ : undefined,
1742
+ );
1743
+ const resolution = workflowStep
1744
+ ? await (
1745
+ workflowStep.do as unknown as (
1746
+ name: string,
1747
+ callback: () => Promise<WorkerStepResolution>,
1748
+ ) => Promise<WorkerStepResolution>
1749
+ )(stepPath.join('.'), runStep)
1750
+ : await runStep();
1660
1751
  const value = resolution.value;
1661
1752
  currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
1662
1753
  if (recorder) {
@@ -2373,6 +2464,18 @@ function childPipelineUsesCtxMap(
2373
2464
  );
2374
2465
  }
2375
2466
 
2467
+ function childPipelineNeedsWorkflowScheduler(
2468
+ pipeline: PlayStaticPipeline | null | undefined,
2469
+ ): boolean {
2470
+ if (!pipeline) return false;
2471
+ return flattenStaticPipeline(pipeline).some(
2472
+ (substep) =>
2473
+ substep.type === 'tool' &&
2474
+ (substep.isEventWait === true ||
2475
+ substep.toolId === 'test_wait_for_event'),
2476
+ );
2477
+ }
2478
+
2376
2479
  function releaseChildPlayConcurrency(
2377
2480
  inFlightByPlayName: Record<string, number>,
2378
2481
  playName: string,
@@ -2395,6 +2498,41 @@ function createMinimalWorkerCtx(
2395
2498
  let playCallCount = 0;
2396
2499
  const parentChildCalls: Record<string, number> = {};
2397
2500
  const inFlightChildCallsByPlayName: Record<string, number> = {};
2501
+ let inFlightChildPlayCalls = 0;
2502
+ const childPlaySlotWaiters: Array<() => void> = [];
2503
+
2504
+ const acquireChildPlaySlot = async (): Promise<() => void> => {
2505
+ while (
2506
+ inFlightChildPlayCalls >= WORKER_PLAY_CALL_LIMITS.maxConcurrentPlayCalls
2507
+ ) {
2508
+ await new Promise<void>((resolve, reject) => {
2509
+ const waiter = () => {
2510
+ abortSignal?.removeEventListener('abort', onAbort);
2511
+ resolve();
2512
+ };
2513
+ const onAbort = () => {
2514
+ const index = childPlaySlotWaiters.indexOf(waiter);
2515
+ if (index >= 0) childPlaySlotWaiters.splice(index, 1);
2516
+ reject(
2517
+ abortSignal?.reason instanceof Error
2518
+ ? abortSignal.reason
2519
+ : new WorkflowAbortError(),
2520
+ );
2521
+ };
2522
+ childPlaySlotWaiters.push(waiter);
2523
+ abortSignal?.addEventListener('abort', onAbort, { once: true });
2524
+ });
2525
+ assertNotAborted(abortSignal);
2526
+ }
2527
+ inFlightChildPlayCalls += 1;
2528
+ let released = false;
2529
+ return () => {
2530
+ if (released) return;
2531
+ released = true;
2532
+ inFlightChildPlayCalls = Math.max(0, inFlightChildPlayCalls - 1);
2533
+ childPlaySlotWaiters.shift()?.();
2534
+ };
2535
+ };
2398
2536
  const rootGovernance = req.playCallGovernance;
2399
2537
  const rootRunId = rootGovernance?.rootRunId ?? req.runId;
2400
2538
  // Local ancestry chain that always ENDS with the currently-executing play
@@ -2491,6 +2629,7 @@ function createMinimalWorkerCtx(
2491
2629
  : JSON.stringify(normalizedParts);
2492
2630
  return keyValue;
2493
2631
  };
2632
+ const mapLogicFingerprint = req.graphHash ?? null;
2494
2633
  const resolveRowKey = (
2495
2634
  row: Record<string, unknown>,
2496
2635
  index: number,
@@ -2498,8 +2637,12 @@ function createMinimalWorkerCtx(
2498
2637
  const inputRow = publicCsvInputRow(row);
2499
2638
  const explicitKeyValue = resolveExplicitKeyValue(row, index);
2500
2639
  return explicitKeyValue == null
2501
- ? derivePlayRowIdentity(inputRow, name)
2502
- : derivePlayRowIdentityFromKey(explicitKeyValue, name);
2640
+ ? derivePlayRowIdentity(inputRow, name, mapLogicFingerprint)
2641
+ : derivePlayRowIdentityFromKey(
2642
+ explicitKeyValue,
2643
+ name,
2644
+ mapLogicFingerprint,
2645
+ );
2503
2646
  };
2504
2647
  const assertUniqueExplicitRowKeys = (
2505
2648
  chunkRows: readonly Record<string, unknown>[],
@@ -2547,7 +2690,11 @@ function createMinimalWorkerCtx(
2547
2690
  const key =
2548
2691
  typeof row.__deeplineRowKey === 'string'
2549
2692
  ? row.__deeplineRowKey
2550
- : derivePlayRowIdentity(publicCsvInputRow(row), name);
2693
+ : derivePlayRowIdentity(
2694
+ publicCsvInputRow(row),
2695
+ name,
2696
+ mapLogicFingerprint,
2697
+ );
2551
2698
  if (key) {
2552
2699
  pendingKeys.add(key);
2553
2700
  preparedKeys.add(key);
@@ -2557,7 +2704,11 @@ function createMinimalWorkerCtx(
2557
2704
  const key =
2558
2705
  typeof row.__deeplineRowKey === 'string'
2559
2706
  ? row.__deeplineRowKey
2560
- : derivePlayRowIdentity(publicCsvInputRow(row), name);
2707
+ : derivePlayRowIdentity(
2708
+ publicCsvInputRow(row),
2709
+ name,
2710
+ mapLogicFingerprint,
2711
+ );
2561
2712
  if (key) {
2562
2713
  completedKeys.add(key);
2563
2714
  preparedKeys.add(key);
@@ -2569,7 +2720,17 @@ function createMinimalWorkerCtx(
2569
2720
  const rowsToExecuteEntries = chunkEntries.filter(
2570
2721
  ({ rowKey }) => pendingKeys.has(rowKey) || !completedKeys.has(rowKey),
2571
2722
  );
2572
- const rowsToExecute = rowsToExecuteEntries.map(({ row }) => row);
2723
+ const uniqueRowsToExecuteEntries = [
2724
+ ...new Map(
2725
+ rowsToExecuteEntries.map((entry) => [entry.rowKey, entry]),
2726
+ ).values(),
2727
+ ];
2728
+ const duplicateInputReuseCount = Math.max(
2729
+ 0,
2730
+ chunkEntries.length -
2731
+ new Set(chunkEntries.map((entry) => entry.rowKey)).size,
2732
+ );
2733
+ const rowsToExecute = uniqueRowsToExecuteEntries.map(({ row }) => row);
2573
2734
  const rowsInserted = prepared.inserted + missingPreparedRows.length;
2574
2735
  const rowsSkipped = Math.max(
2575
2736
  0,
@@ -2593,7 +2754,7 @@ function createMinimalWorkerCtx(
2593
2754
  if (abortSignal?.aborted) return;
2594
2755
  const myIndex = idx++;
2595
2756
  if (myIndex >= rowsToExecute.length) return;
2596
- const entry = rowsToExecuteEntries[myIndex]!;
2757
+ const entry = uniqueRowsToExecuteEntries[myIndex]!;
2597
2758
  const row = entry.row;
2598
2759
  const absoluteIndex = entry.absoluteIndex;
2599
2760
  const enriched: Record<string, unknown> = cloneCsvAliasedRow(row);
@@ -2701,14 +2862,33 @@ function createMinimalWorkerCtx(
2701
2862
  })(),
2702
2863
  );
2703
2864
  }
2704
- await Promise.all(workers);
2705
- if (executedRows.length > 0) {
2865
+ const persistExecutedRows = async () => {
2866
+ const rowsToPersist = executedRows
2867
+ .map((row, executedIndex) =>
2868
+ row
2869
+ ? {
2870
+ row,
2871
+ executedIndex,
2872
+ }
2873
+ : null,
2874
+ )
2875
+ .filter(
2876
+ (
2877
+ entry,
2878
+ ): entry is {
2879
+ row: T & Record<string, unknown>;
2880
+ executedIndex: number;
2881
+ } => entry !== null,
2882
+ );
2883
+ if (rowsToPersist.length === 0) {
2884
+ return;
2885
+ }
2706
2886
  await persistCompletedMapRows({
2707
2887
  req,
2708
2888
  tableNamespace: name,
2709
2889
  outputFields,
2710
2890
  extraOutputFields: Array.from(generatedOutputFields),
2711
- rows: executedRows.map((row, executedIndex) => ({
2891
+ rows: rowsToPersist.map(({ row, executedIndex }) => ({
2712
2892
  ...row,
2713
2893
  ...(executedCellMetaPatches[executedIndex]
2714
2894
  ? {
@@ -2716,16 +2896,30 @@ function createMinimalWorkerCtx(
2716
2896
  executedCellMetaPatches[executedIndex],
2717
2897
  }
2718
2898
  : {}),
2719
- __deeplineRowKey: rowsToExecuteEntries[executedIndex]!.rowKey,
2899
+ __deeplineRowKey:
2900
+ uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
2720
2901
  })),
2721
2902
  });
2903
+ };
2904
+ const workerResults = await Promise.allSettled(workers);
2905
+ await persistExecutedRows();
2906
+ const rejectedWorker = workerResults.find(
2907
+ (result): result is PromiseRejectedResult =>
2908
+ result.status === 'rejected',
2909
+ );
2910
+ if (rejectedWorker) {
2911
+ throw rejectedWorker.reason;
2722
2912
  }
2723
2913
  const resultByKey = new Map<string, T & Record<string, unknown>>();
2724
2914
  for (const completedRow of prepared.completedRows) {
2725
2915
  const key =
2726
2916
  typeof completedRow.__deeplineRowKey === 'string'
2727
2917
  ? completedRow.__deeplineRowKey
2728
- : derivePlayRowIdentity(publicCsvInputRow(completedRow), name);
2918
+ : derivePlayRowIdentity(
2919
+ publicCsvInputRow(completedRow),
2920
+ name,
2921
+ mapLogicFingerprint,
2922
+ );
2729
2923
  if (key) {
2730
2924
  const { __deeplineRowKey: _rowKey, ...cleanedRow } =
2731
2925
  publicCsvInputRow(completedRow);
@@ -2739,7 +2933,7 @@ function createMinimalWorkerCtx(
2739
2933
  executedIndex += 1
2740
2934
  ) {
2741
2935
  const executedRow = executedRows[executedIndex]!;
2742
- const key = rowsToExecuteEntries[executedIndex]!.rowKey;
2936
+ const key = uniqueRowsToExecuteEntries[executedIndex]!.rowKey;
2743
2937
  if (key) resultByKey.set(key, executedRow);
2744
2938
  }
2745
2939
  const out = chunkRows
@@ -2755,7 +2949,8 @@ function createMinimalWorkerCtx(
2755
2949
  rowsRead: chunkRows.length,
2756
2950
  rowsWritten: out.length,
2757
2951
  rowsExecuted: executedRows.length,
2758
- rowsCached: prepared.completedRows.length,
2952
+ rowsCached: Math.max(0, out.length - executedRows.length),
2953
+ rowsDuplicateReused: duplicateInputReuseCount,
2759
2954
  rowsInserted,
2760
2955
  rowsSkipped,
2761
2956
  outputDatasetId: `map:${name}`,
@@ -2767,6 +2962,7 @@ function createMinimalWorkerCtx(
2767
2962
  const out: Array<T & Record<string, unknown>> = [];
2768
2963
  let totalRowsExecuted = 0;
2769
2964
  let totalRowsCached = 0;
2965
+ let totalRowsDuplicateReused = 0;
2770
2966
  let totalRowsInserted = 0;
2771
2967
  let totalRowsSkipped = 0;
2772
2968
 
@@ -2809,6 +3005,17 @@ function createMinimalWorkerCtx(
2809
3005
  return makeWorkerDataset(name, out, {
2810
3006
  count: totalRowsWritten,
2811
3007
  cacheSummary,
3008
+ workProgress: {
3009
+ total: totalRowsWritten,
3010
+ executed: totalRowsExecuted,
3011
+ reused: totalRowsCached,
3012
+ skipped: totalRowsCached,
3013
+ pending: 0,
3014
+ failed: 0,
3015
+ ...(totalRowsDuplicateReused > 0
3016
+ ? { duplicates: { exact: totalRowsDuplicateReused } }
3017
+ : {}),
3018
+ },
2812
3019
  });
2813
3020
  };
2814
3021
 
@@ -2829,6 +3036,7 @@ function createMinimalWorkerCtx(
2829
3036
  totalRowsWritten += chunkResult.rowsWritten;
2830
3037
  totalRowsExecuted += chunkResult.rowsExecuted;
2831
3038
  totalRowsCached += chunkResult.rowsCached;
3039
+ totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
2832
3040
  totalRowsInserted += chunkResult.rowsInserted;
2833
3041
  totalRowsSkipped += chunkResult.rowsSkipped;
2834
3042
  if (out.length < 10) {
@@ -2852,6 +3060,7 @@ function createMinimalWorkerCtx(
2852
3060
  totalRowsWritten += chunkResult.rowsWritten;
2853
3061
  totalRowsExecuted += chunkResult.rowsExecuted;
2854
3062
  totalRowsCached += chunkResult.rowsCached;
3063
+ totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
2855
3064
  totalRowsInserted += chunkResult.rowsInserted;
2856
3065
  totalRowsSkipped += chunkResult.rowsSkipped;
2857
3066
  if (out.length < 10) {
@@ -2865,6 +3074,7 @@ function createMinimalWorkerCtx(
2865
3074
  const chunkResult = await runChunkStep(sliced, 0, 0);
2866
3075
  totalRowsExecuted = chunkResult.rowsExecuted;
2867
3076
  totalRowsCached = chunkResult.rowsCached;
3077
+ totalRowsDuplicateReused = chunkResult.rowsDuplicateReused;
2868
3078
  totalRowsInserted = chunkResult.rowsInserted;
2869
3079
  totalRowsSkipped = chunkResult.rowsSkipped;
2870
3080
  out.push(...chunkResult.preview);
@@ -2917,18 +3127,39 @@ function createMinimalWorkerCtx(
2917
3127
  },
2918
3128
  async step<T>(name: string, callback: () => Promise<T> | T): Promise<T> {
2919
3129
  assertNotAborted(abortSignal);
2920
- if (!workflowStep) {
2921
- return await callback();
3130
+ if (!name.trim()) {
3131
+ throw new Error('ctx.step(name, callback) requires a name.');
2922
3132
  }
2923
- return await (
2924
- workflowStep.do as unknown as (
2925
- name: string,
2926
- callback: () => Promise<T>,
2927
- ) => Promise<T>
2928
- )(name, async () => {
2929
- assertNotAborted(abortSignal);
2930
- return await callback();
2931
- });
3133
+ // Static pipeline JS blocks are already Workflow steps in the Workers
3134
+ // backend. Nesting another `step.do` here can leave preview runs parked
3135
+ // inside the JS stage before they reach subsequent event waits.
3136
+ return await callback();
3137
+ },
3138
+ async runSteps<T>(
3139
+ program: WorkerStepProgram,
3140
+ input: Record<string, unknown>,
3141
+ opts?: { description?: string },
3142
+ ): Promise<T> {
3143
+ assertNotAborted(abortSignal);
3144
+ if (!isWorkerStepProgram(program)) {
3145
+ throw new Error('ctx.runSteps(program, input) requires steps().');
3146
+ }
3147
+ if (opts?.description) {
3148
+ emitEvent({
3149
+ type: 'log',
3150
+ level: 'info',
3151
+ message: String(opts.description),
3152
+ ts: nowMs(),
3153
+ });
3154
+ }
3155
+ return (await executeWorkerStepProgram(
3156
+ program,
3157
+ input,
3158
+ ctx,
3159
+ 0,
3160
+ undefined,
3161
+ workflowStep,
3162
+ )) as T;
2932
3163
  },
2933
3164
  async csv<T extends Record<string, unknown> = Record<string, unknown>>(
2934
3165
  arg: unknown,
@@ -3063,7 +3294,11 @@ function createMinimalWorkerCtx(
3063
3294
  const completedKeys = new Set<string>();
3064
3295
  const preparedKeys = new Set<string>();
3065
3296
  for (const row of prepared.pendingRows) {
3066
- const key = derivePlayRowIdentity(publicCsvInputRow(row), name);
3297
+ const key = derivePlayRowIdentity(
3298
+ publicCsvInputRow(row),
3299
+ name,
3300
+ mapLogicFingerprint,
3301
+ );
3067
3302
  if (key) {
3068
3303
  pendingKeys.add(key);
3069
3304
  preparedKeys.add(key);
@@ -3073,18 +3308,30 @@ function createMinimalWorkerCtx(
3073
3308
  const key =
3074
3309
  typeof row.__deeplineRowKey === 'string'
3075
3310
  ? row.__deeplineRowKey
3076
- : derivePlayRowIdentity(publicCsvInputRow(row), name);
3311
+ : derivePlayRowIdentity(
3312
+ publicCsvInputRow(row),
3313
+ name,
3314
+ mapLogicFingerprint,
3315
+ );
3077
3316
  if (key) {
3078
3317
  completedKeys.add(key);
3079
3318
  preparedKeys.add(key);
3080
3319
  }
3081
3320
  }
3082
3321
  const missingPreparedRows = chunkRows.filter((row) => {
3083
- const key = derivePlayRowIdentity(publicCsvInputRow(row), name);
3322
+ const key = derivePlayRowIdentity(
3323
+ publicCsvInputRow(row),
3324
+ name,
3325
+ mapLogicFingerprint,
3326
+ );
3084
3327
  return !key || !preparedKeys.has(key);
3085
3328
  });
3086
3329
  const rowsToExecute = chunkRows.filter((row) => {
3087
- const key = derivePlayRowIdentity(publicCsvInputRow(row), name);
3330
+ const key = derivePlayRowIdentity(
3331
+ publicCsvInputRow(row),
3332
+ name,
3333
+ mapLogicFingerprint,
3334
+ );
3088
3335
  return !key || pendingKeys.has(key) || !completedKeys.has(key);
3089
3336
  });
3090
3337
  const rowsInserted = prepared.inserted + missingPreparedRows.length;
@@ -3167,6 +3414,7 @@ function createMinimalWorkerCtx(
3167
3414
  __deeplineRowKey: derivePlayRowIdentity(
3168
3415
  publicCsvInputRow(rowsToExecute[executedIndex]!),
3169
3416
  name,
3417
+ mapLogicFingerprint,
3170
3418
  ),
3171
3419
  })),
3172
3420
  });
@@ -3176,7 +3424,11 @@ function createMinimalWorkerCtx(
3176
3424
  const key =
3177
3425
  typeof completedRow.__deeplineRowKey === 'string'
3178
3426
  ? completedRow.__deeplineRowKey
3179
- : derivePlayRowIdentity(publicCsvInputRow(completedRow), name);
3427
+ : derivePlayRowIdentity(
3428
+ publicCsvInputRow(completedRow),
3429
+ name,
3430
+ mapLogicFingerprint,
3431
+ );
3180
3432
  if (key) {
3181
3433
  const { __deeplineRowKey: _rowKey, ...cleanedRow } =
3182
3434
  publicCsvInputRow(completedRow);
@@ -3193,12 +3445,17 @@ function createMinimalWorkerCtx(
3193
3445
  const key = derivePlayRowIdentity(
3194
3446
  publicCsvInputRow(rowsToExecute[executedIndex]!),
3195
3447
  name,
3448
+ mapLogicFingerprint,
3196
3449
  );
3197
3450
  if (key) resultByKey.set(key, executedRow);
3198
3451
  }
3199
3452
  const out = chunkRows
3200
3453
  .map((row) => {
3201
- const key = derivePlayRowIdentity(publicCsvInputRow(row), name);
3454
+ const key = derivePlayRowIdentity(
3455
+ publicCsvInputRow(row),
3456
+ name,
3457
+ mapLogicFingerprint,
3458
+ );
3202
3459
  return key ? resultByKey.get(key) : undefined;
3203
3460
  })
3204
3461
  .filter((row): row is T & Record<string, unknown> => Boolean(row));
@@ -3263,6 +3520,14 @@ function createMinimalWorkerCtx(
3263
3520
  return makeWorkerDataset(name, out, {
3264
3521
  count: totalRowsWritten,
3265
3522
  cacheSummary,
3523
+ workProgress: {
3524
+ total: totalRowsWritten,
3525
+ executed: totalRowsExecuted,
3526
+ reused: totalRowsCached,
3527
+ skipped: totalRowsCached,
3528
+ pending: 0,
3529
+ failed: 0,
3530
+ },
3266
3531
  });
3267
3532
  };
3268
3533
 
@@ -3448,7 +3713,11 @@ function createMinimalWorkerCtx(
3448
3713
  const childIsMapBacked = childPipelineUsesCtxMap(
3449
3714
  childManifest.staticPipeline,
3450
3715
  );
3716
+ const childNeedsWorkflowScheduler = childPipelineNeedsWorkflowScheduler(
3717
+ childManifest.staticPipeline,
3718
+ );
3451
3719
  let childConcurrencyAcquired = false;
3720
+ let releaseChildPlaySlot: (() => void) | null = null;
3452
3721
  if (childIsMapBacked) {
3453
3722
  const nextInFlight =
3454
3723
  (inFlightChildCallsByPlayName[resolvedName] ?? 0) + 1;
@@ -3463,11 +3732,21 @@ function createMinimalWorkerCtx(
3463
3732
  childConcurrencyAcquired = true;
3464
3733
  }
3465
3734
  try {
3735
+ releaseChildPlaySlot = await acquireChildPlaySlot();
3466
3736
  const childSubmitStartedAt = nowMs();
3467
- let started: { workflowId?: string; runId?: string; error?: unknown };
3737
+ let started: {
3738
+ workflowId?: string;
3739
+ runId?: string;
3740
+ status?: string;
3741
+ output?: unknown;
3742
+ result?: unknown;
3743
+ error?: unknown;
3744
+ };
3468
3745
  try {
3469
3746
  started = await submitChildPlayThroughCoordinator({
3470
3747
  req,
3748
+ allowInline:
3749
+ options?.timeoutMs == null && !childNeedsWorkflowScheduler,
3471
3750
  body: {
3472
3751
  name: resolvedName,
3473
3752
  input: isRecord(input) ? input : {},
@@ -3537,6 +3816,27 @@ function createMinimalWorkerCtx(
3537
3816
  ms: nowMs() - childSubmitStartedAt,
3538
3817
  status: 'ok',
3539
3818
  });
3819
+ const startedStatus = String(started.status ?? '').toLowerCase();
3820
+ if (startedStatus === 'completed') {
3821
+ emitEvent({
3822
+ type: 'log',
3823
+ level: 'info',
3824
+ message: `Completed child play ${resolvedName} (${normalizedKey})`,
3825
+ ts: nowMs(),
3826
+ });
3827
+ return started.output ?? extractChildPlayOutput(started);
3828
+ }
3829
+ if (startedStatus === 'failed') {
3830
+ const startedError = isRecord(started.error)
3831
+ ? started.error
3832
+ : { message: started.error };
3833
+ const startedErrorMessage =
3834
+ typeof startedError.message === 'string' &&
3835
+ startedError.message.trim()
3836
+ ? startedError.message.trim()
3837
+ : `Child play ${resolvedName} (${workflowId}) failed.`;
3838
+ throw new Error(startedErrorMessage);
3839
+ }
3540
3840
  const childWaitStartedAt = nowMs();
3541
3841
  let result: unknown;
3542
3842
  try {
@@ -3589,6 +3889,7 @@ function createMinimalWorkerCtx(
3589
3889
  });
3590
3890
  return result;
3591
3891
  } finally {
3892
+ releaseChildPlaySlot?.();
3592
3893
  if (childConcurrencyAcquired) {
3593
3894
  releaseChildPlayConcurrency(
3594
3895
  inFlightChildCallsByPlayName,
@@ -3619,11 +3920,11 @@ function createMinimalWorkerCtx(
3619
3920
  const event = (await (
3620
3921
  workflowStep.waitForEvent as unknown as (
3621
3922
  name: string,
3622
- options: { type: string; timeout: number },
3923
+ options: { type: string; timeout: string },
3623
3924
  ) => Promise<{ payload: unknown }>
3624
3925
  )(`wait_for_event:${workflowEventType(eventType)}`, {
3625
3926
  type: workflowEventType(eventType),
3626
- timeout: timeoutMs,
3927
+ timeout: workflowTimeoutFromMs(timeoutMs),
3627
3928
  })) as { payload: unknown };
3628
3929
  return event.payload ?? null;
3629
3930
  },
@@ -3804,6 +4105,9 @@ async function executeRunRequest(
3804
4105
  status: 'completed',
3805
4106
  result: terminalResult,
3806
4107
  runtimeBackend: 'cf_workflows_dynamic_worker',
4108
+ waitKind: null,
4109
+ waitUntil: null,
4110
+ activeBoundaryId: null,
3807
4111
  liveLogs,
3808
4112
  lastCheckpointAt: nowMs(),
3809
4113
  });
@@ -3825,6 +4129,7 @@ async function executeRunRequest(
3825
4129
  );
3826
4130
  });
3827
4131
  return {
4132
+ playName: req.playName,
3828
4133
  result: serializedResult,
3829
4134
  outputRows: inferOutputRows(serializedResult),
3830
4135
  durationMs: nowMs() - startedAt,
@@ -3847,6 +4152,9 @@ async function executeRunRequest(
3847
4152
  status: aborted ? 'cancelled' : 'failed',
3848
4153
  error: message,
3849
4154
  runtimeBackend: 'cf_workflows_dynamic_worker',
4155
+ waitKind: null,
4156
+ waitUntil: null,
4157
+ activeBoundaryId: null,
3850
4158
  liveLogs,
3851
4159
  lastCheckpointAt: nowMs(),
3852
4160
  });
@@ -4121,6 +4429,14 @@ function serializeValue(value: unknown, depth: number): unknown {
4121
4429
  ? (value as unknown as { __deeplineCacheSummary: string })
4122
4430
  .__deeplineCacheSummary
4123
4431
  : null;
4432
+ const workProgress =
4433
+ isRecord(
4434
+ (value as unknown as { __deeplineWorkProgress?: unknown })
4435
+ .__deeplineWorkProgress,
4436
+ )
4437
+ ? (value as unknown as { __deeplineWorkProgress: Record<string, unknown> })
4438
+ .__deeplineWorkProgress
4439
+ : null;
4124
4440
  const previewRows = value
4125
4441
  .slice(0, 5)
4126
4442
  .map((row) => serializeValue(row, depth + 1))
@@ -4138,6 +4454,9 @@ function serializeValue(value: unknown, depth: number): unknown {
4138
4454
  preview: previewRows,
4139
4455
  tableNamespace,
4140
4456
  ...(cacheSummary ? { cacheSummary } : {}),
4457
+ ...(workProgress
4458
+ ? { _metadata: { workProgress } }
4459
+ : {}),
4141
4460
  };
4142
4461
  }
4143
4462
  return value.map((entry) => serializeValue(entry, depth + 1));