deepline 0.1.83 → 0.1.88

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.
@@ -163,11 +163,19 @@ import {
163
163
  } from './runtime/tool-http-errors';
164
164
  import {
165
165
  StepProgramDatasetBuilder,
166
+ type StepProgramDatasetColumnInput,
166
167
  type StepProgramDatasetOptions,
167
168
  } from '../../../shared_libs/play-runtime/step-program-dataset-builder';
168
169
  import {
169
170
  DEEPLINE_CELL_META_FIELD,
171
+ normalizeCellStalenessPolicy,
172
+ previousCellFromValue,
173
+ resolveCompletedCellStalenessMeta,
170
174
  shouldRecomputeCell,
175
+ type AuthoredCellStalenessPolicyByField,
176
+ type AuthoredStaleAfterSeconds,
177
+ type CellStalenessPolicyByField,
178
+ type PreviousCell,
171
179
  } from '../../../shared_libs/play-runtime/cell-staleness';
172
180
 
173
181
  // The play's default export. The bundler injects this — see bundle-play-file.ts.
@@ -592,7 +600,7 @@ type WorkerCtxCallbacks = {
592
600
  nodeId: string;
593
601
  progress: LiveNodeProgressSnapshot;
594
602
  forceFlush?: boolean;
595
- }) => void;
603
+ }) => void | Promise<void>;
596
604
  onMapStarted?: (nodeId: string, at?: number) => void;
597
605
  onMapCompleted?: (nodeId: string, at?: number) => void;
598
606
  onToolCalled?: (toolId: string, at?: number) => void;
@@ -717,10 +725,7 @@ function publicCsvStorageRow<T extends Record<string, unknown>>(row: T): T {
717
725
  storageRow[fieldName] =
718
726
  'value' in descriptor ? descriptor.value : publicRow[fieldName];
719
727
  }
720
- for (const runtimeField of [
721
- '__deeplineRowKey',
722
- '__deeplineCellMetaPatch',
723
- ]) {
728
+ for (const runtimeField of ['__deeplineRowKey', '__deeplineCellMetaPatch']) {
724
729
  if (Object.prototype.hasOwnProperty.call(row, runtimeField)) {
725
730
  storageRow[runtimeField] = row[runtimeField];
726
731
  }
@@ -1587,6 +1592,7 @@ type WorkerToolBatchRequest = {
1587
1592
  };
1588
1593
 
1589
1594
  const WORKER_TOOL_BATCH_GRACE_MS = 250;
1595
+ const MAP_EXECUTION_HEARTBEAT_INTERVAL_MS = 5_000;
1590
1596
  // Fallback batch-chunk parallelism when a tool declares no provider rate hints.
1591
1597
  // Matches the prior hardcoded `Math.min(4, ...)` so undeclared providers keep
1592
1598
  // their previous batching behavior; declared providers tighten via the
@@ -1594,6 +1600,10 @@ const WORKER_TOOL_BATCH_GRACE_MS = 250;
1594
1600
  const WORKER_TOOL_BATCH_DEFAULT_PARALLELISM = 4;
1595
1601
  const WORKER_RETRY_SAFE_5XX_TOOLS = new Set(['test_transient_500']);
1596
1602
 
1603
+ function sleepWorkerMs(ms: number): Promise<void> {
1604
+ return new Promise((resolve) => setTimeout(resolve, ms));
1605
+ }
1606
+
1597
1607
  function stepProgramColumnName(parentField: string, stepId: string): string {
1598
1608
  return sqlSafePlayColumnName(`${parentField}.${stepId}`);
1599
1609
  }
@@ -1973,6 +1983,7 @@ type WorkerStepResolver = (
1973
1983
  row: Record<string, unknown>,
1974
1984
  ctx: unknown,
1975
1985
  index: number,
1986
+ previousCell?: PreviousCell,
1976
1987
  ) => Promise<unknown> | unknown;
1977
1988
 
1978
1989
  type WorkerConditionalStepResolver = {
@@ -1987,7 +1998,7 @@ type WorkerConditionalStepResolver = {
1987
1998
 
1988
1999
  type WorkerStepProgramStep = {
1989
2000
  name: string;
1990
- staleAfterSeconds?: number;
2001
+ staleAfterSeconds?: AuthoredStaleAfterSeconds;
1991
2002
  resolver:
1992
2003
  | WorkerStepResolver
1993
2004
  | WorkerConditionalStepResolver
@@ -2042,6 +2053,7 @@ async function executeWorkerStepResolver(
2042
2053
  row: Record<string, unknown>,
2043
2054
  ctx: unknown,
2044
2055
  index: number,
2056
+ previousCell?: PreviousCell,
2045
2057
  recorder?: {
2046
2058
  parentField: string;
2047
2059
  path: string[];
@@ -2051,7 +2063,14 @@ async function executeWorkerStepResolver(
2051
2063
  if (isWorkerConditionalStepResolver(resolver)) {
2052
2064
  const shouldRun = await resolver.when(row, index);
2053
2065
  return shouldRun
2054
- ? await executeWorkerStepResolver(resolver.run, row, ctx, index, recorder)
2066
+ ? await executeWorkerStepResolver(
2067
+ resolver.run,
2068
+ row,
2069
+ ctx,
2070
+ index,
2071
+ previousCell,
2072
+ recorder,
2073
+ )
2055
2074
  : {
2056
2075
  value: resolver.elseValue ?? null,
2057
2076
  status: 'skipped',
@@ -2069,7 +2088,14 @@ async function executeWorkerStepResolver(
2069
2088
  };
2070
2089
  }
2071
2090
  if (typeof resolver === 'function') {
2072
- return { value: await (resolver as WorkerStepResolver)(row, ctx, index) };
2091
+ return {
2092
+ value: await (resolver as WorkerStepResolver)(
2093
+ row,
2094
+ ctx,
2095
+ index,
2096
+ previousCell,
2097
+ ),
2098
+ };
2073
2099
  }
2074
2100
  return { value: resolver };
2075
2101
  }
@@ -2095,6 +2121,7 @@ async function executeWorkerStepProgram(
2095
2121
  currentRow,
2096
2122
  ctx,
2097
2123
  index,
2124
+ undefined,
2098
2125
  recorder
2099
2126
  ? {
2100
2127
  ...recorder,
@@ -3005,7 +3032,7 @@ async function prepareMapRows(input: {
3005
3032
  tableNamespace: string;
3006
3033
  rows: Record<string, unknown>[];
3007
3034
  outputFields: string[];
3008
- cellPolicies?: Record<string, { staleAfterSeconds?: number }>;
3035
+ cellPolicies?: CellStalenessPolicyByField;
3009
3036
  }): Promise<{
3010
3037
  inserted: number;
3011
3038
  skipped: number;
@@ -3161,18 +3188,21 @@ function createCoordinatorRatePort(req: RunRequest): CoordinatorRatePort {
3161
3188
  if (!coordinatorUrl) {
3162
3189
  throw new Error('Coordinator rate acquire is unavailable.');
3163
3190
  }
3164
- const res = await fetch(`${coordinatorUrl.replace(/\/$/, '')}/rate-acquire`, {
3165
- method: 'POST',
3166
- headers: {
3167
- 'x-deepline-request-id': makeRequestId(),
3168
- ...coordinatorRequestHeaders({
3169
- runId: req.runId,
3170
- contentType: 'application/json',
3171
- internalToken: req.coordinatorInternalToken,
3172
- }),
3191
+ const res = await fetch(
3192
+ `${coordinatorUrl.replace(/\/$/, '')}/rate-acquire`,
3193
+ {
3194
+ method: 'POST',
3195
+ headers: {
3196
+ 'x-deepline-request-id': makeRequestId(),
3197
+ ...coordinatorRequestHeaders({
3198
+ runId: req.runId,
3199
+ contentType: 'application/json',
3200
+ internalToken: req.coordinatorInternalToken,
3201
+ }),
3202
+ },
3203
+ body: JSON.stringify(input),
3173
3204
  },
3174
- body: JSON.stringify(input),
3175
- });
3205
+ );
3176
3206
  if (!res.ok) {
3177
3207
  const text = await res.text().catch(() => '');
3178
3208
  throw new Error(
@@ -3494,7 +3524,8 @@ function createMinimalWorkerCtx(
3494
3524
  index: number,
3495
3525
  ) => Promise<unknown> | unknown)
3496
3526
  >,
3497
- cellPolicies?: Record<string, { staleAfterSeconds?: number }>,
3527
+ cellPolicies?: CellStalenessPolicyByField,
3528
+ authoredCellPolicies?: AuthoredCellStalenessPolicyByField,
3498
3529
  opts?: WorkerMapOptions,
3499
3530
  ): Promise<unknown> => {
3500
3531
  const mapStartedAt = nowMs();
@@ -3516,8 +3547,11 @@ function createMinimalWorkerCtx(
3516
3547
  softWorkflowStepBudget: plan?.chunkPlan.softWorkflowStepBudget,
3517
3548
  });
3518
3549
  const outputFields = fieldEntries.map(([field]) => field);
3519
- const updateMapProgress = (progress: LiveNodeProgressSnapshot) => {
3520
- callbacks?.onNodeProgress?.({
3550
+ const updateMapProgress = (
3551
+ progress: LiveNodeProgressSnapshot,
3552
+ options?: { forceFlush?: boolean },
3553
+ ): void | Promise<void> => {
3554
+ return callbacks?.onNodeProgress?.({
3521
3555
  nodeId: mapNodeId,
3522
3556
  progress: {
3523
3557
  artifactTableNamespace: name,
@@ -3525,19 +3559,62 @@ function createMinimalWorkerCtx(
3525
3559
  ...progress,
3526
3560
  updatedAt: progress.updatedAt ?? nowMs(),
3527
3561
  },
3528
- forceFlush: true,
3562
+ forceFlush: options?.forceFlush ?? true,
3529
3563
  });
3530
3564
  };
3531
3565
  const formatMapProgressMessage = (completed: number, total?: number) =>
3532
3566
  typeof total === 'number' && Number.isFinite(total) && total > 0
3533
3567
  ? `${completed.toLocaleString()} / ${total.toLocaleString()} rows processed`
3534
3568
  : `${completed.toLocaleString()} rows processed`;
3569
+ const formatMapPreparingMessage = (total?: number) =>
3570
+ typeof total === 'number' && Number.isFinite(total) && total > 0
3571
+ ? `Preparing ${total.toLocaleString()} rows`
3572
+ : 'Preparing rows';
3573
+ const formatMapQueuedMessage = (input: {
3574
+ completed: number;
3575
+ queued: number;
3576
+ total?: number;
3577
+ }) => {
3578
+ const completed = Math.max(0, input.completed);
3579
+ const queued = Math.max(0, input.queued);
3580
+ if (completed > 0 && queued > 0) {
3581
+ return `${completed.toLocaleString()} already satisfied, ${queued.toLocaleString()} queued`;
3582
+ }
3583
+ if (queued > 0) {
3584
+ return `${queued.toLocaleString()} rows queued`;
3585
+ }
3586
+ return formatMapProgressMessage(completed, input.total);
3587
+ };
3588
+ const formatMapProcessingMessage = (rowsToExecute: number) =>
3589
+ rowsToExecute > 0
3590
+ ? `Processing ${rowsToExecute.toLocaleString()} rows`
3591
+ : null;
3592
+ const formatMapExecutionHeartbeatMessage = (input: {
3593
+ rowsToExecute: number;
3594
+ startedRows: number;
3595
+ activeRows: number;
3596
+ completedRows: number;
3597
+ }) => {
3598
+ const rowsToExecute = Math.max(0, input.rowsToExecute);
3599
+ const startedRows = Math.max(0, input.startedRows);
3600
+ const activeRows = Math.max(0, input.activeRows);
3601
+ const completedRows = Math.max(0, input.completedRows);
3602
+ const waitingRows = Math.max(0, rowsToExecute - startedRows);
3603
+ const parts = [
3604
+ activeRows > 0 ? `${activeRows.toLocaleString()} active` : null,
3605
+ waitingRows > 0 ? `${waitingRows.toLocaleString()} waiting` : null,
3606
+ completedRows > 0 ? `${completedRows.toLocaleString()} done` : null,
3607
+ ].filter((part): part is string => Boolean(part));
3608
+ const base =
3609
+ formatMapProcessingMessage(rowsToExecute) ?? 'Processing rows';
3610
+ return parts.length > 0 ? `${base} (${parts.join(', ')})` : base;
3611
+ };
3535
3612
  callbacks?.onMapStarted?.(mapNodeId, mapStartedAt);
3536
- updateMapProgress({
3613
+ await updateMapProgress({
3537
3614
  completed: 0,
3538
3615
  total: rowCountHint ?? undefined,
3539
3616
  startedAt: mapStartedAt,
3540
- message: formatMapProgressMessage(0, rowCountHint ?? undefined),
3617
+ message: formatMapPreparingMessage(rowCountHint ?? undefined),
3541
3618
  });
3542
3619
  const explicitRowKeysSeen =
3543
3620
  opts?.key === undefined ? null : new Map<string, number>();
@@ -3611,6 +3688,8 @@ function createMinimalWorkerCtx(
3611
3688
  }
3612
3689
  };
3613
3690
 
3691
+ let totalRowsWritten = 0;
3692
+
3614
3693
  const processChunk = async (
3615
3694
  chunkRows: T[],
3616
3695
  chunkStart: number,
@@ -3655,17 +3734,20 @@ function createMinimalWorkerCtx(
3655
3734
  completedRows: prepared.completedRows.length,
3656
3735
  },
3657
3736
  });
3658
- updateMapProgress({
3659
- completed: prepared.completedRows.length,
3660
- total: chunkRows.length,
3737
+ const progressTotalRows = rowCountHint ?? chunkRows.length;
3738
+ const preparedCompletedRows = Math.min(
3739
+ progressTotalRows,
3740
+ totalRowsWritten + prepared.completedRows.length,
3741
+ );
3742
+ await updateMapProgress({
3743
+ completed: preparedCompletedRows,
3744
+ total: progressTotalRows,
3661
3745
  startedAt: mapStartedAt,
3662
- message:
3663
- prepared.pendingRows.length > 0
3664
- ? `${prepared.pendingRows.length.toLocaleString()} rows queued`
3665
- : formatMapProgressMessage(
3666
- prepared.completedRows.length,
3667
- chunkRows.length,
3668
- ),
3746
+ message: formatMapQueuedMessage({
3747
+ completed: preparedCompletedRows,
3748
+ queued: prepared.pendingRows.length,
3749
+ total: progressTotalRows,
3750
+ }),
3669
3751
  });
3670
3752
  const pendingKeys = new Set<string>();
3671
3753
  const pendingRowsByKey = new Map<string, Record<string, unknown>>();
@@ -3709,38 +3791,83 @@ function createMinimalWorkerCtx(
3709
3791
  new Set(chunkEntries.map((entry) => entry.rowKey)).size,
3710
3792
  );
3711
3793
  const rowsToExecute = uniqueRowsToExecuteEntries.map(({ row }) => row);
3794
+ const processingMessage = formatMapProcessingMessage(
3795
+ rowsToExecute.length,
3796
+ );
3797
+ if (processingMessage) {
3798
+ await updateMapProgress({
3799
+ completed: preparedCompletedRows,
3800
+ total: progressTotalRows,
3801
+ startedAt: mapStartedAt,
3802
+ message: processingMessage,
3803
+ });
3804
+ }
3712
3805
  const rowsInserted = prepared.inserted + missingPreparedRows.length;
3713
3806
  const rowsSkipped = Math.max(
3714
3807
  0,
3715
3808
  prepared.skipped - missingPreparedRows.length,
3716
3809
  );
3717
- let settledToolRequests = 0;
3718
- let lastToolProgressAt = 0;
3719
- const reportSettledToolRequests = (count: number) => {
3720
- if (count <= 0) return;
3721
- settledToolRequests += count;
3810
+ let completedExecutedRows = 0;
3811
+ let startedExecutedRows = 0;
3812
+ let activeExecutedRows = 0;
3813
+ let lastChunkProgressAt = 0;
3814
+ let lastExecutionHeartbeatAt = nowMs();
3815
+ const completedRowsForProgress = () =>
3816
+ Math.min(
3817
+ progressTotalRows,
3818
+ totalRowsWritten +
3819
+ prepared.completedRows.length +
3820
+ completedExecutedRows,
3821
+ );
3822
+ const reportExecutionHeartbeat = (force = false) => {
3722
3823
  const now = nowMs();
3723
- const estimatedCompleted = Math.min(
3724
- chunkRows.length,
3725
- prepared.completedRows.length + settledToolRequests,
3824
+ if (
3825
+ !force &&
3826
+ now - lastExecutionHeartbeatAt < MAP_EXECUTION_HEARTBEAT_INTERVAL_MS
3827
+ ) {
3828
+ return;
3829
+ }
3830
+ lastExecutionHeartbeatAt = now;
3831
+ void updateMapProgress(
3832
+ {
3833
+ completed: completedRowsForProgress(),
3834
+ total: progressTotalRows,
3835
+ startedAt: mapStartedAt,
3836
+ message: formatMapExecutionHeartbeatMessage({
3837
+ rowsToExecute: rowsToExecute.length,
3838
+ startedRows: startedExecutedRows,
3839
+ activeRows: activeExecutedRows,
3840
+ completedRows: completedExecutedRows,
3841
+ }),
3842
+ },
3843
+ { forceFlush: force },
3726
3844
  );
3727
- const isTerminalEstimate = estimatedCompleted >= chunkRows.length;
3845
+ };
3846
+ const reportChunkProgress = (force = false) => {
3847
+ const now = nowMs();
3848
+ const completed = completedRowsForProgress();
3849
+ const isTerminalEstimate = completed >= progressTotalRows;
3728
3850
  if (
3851
+ !force &&
3729
3852
  !isTerminalEstimate &&
3730
- now - lastToolProgressAt < RUN_LEDGER_FLUSH_INTERVAL_MS
3853
+ now - lastChunkProgressAt < RUN_LEDGER_FLUSH_INTERVAL_MS
3731
3854
  ) {
3732
3855
  return;
3733
3856
  }
3734
- lastToolProgressAt = now;
3735
- updateMapProgress({
3736
- completed: estimatedCompleted,
3737
- total: chunkRows.length,
3738
- startedAt: mapStartedAt,
3739
- message: formatMapProgressMessage(
3740
- estimatedCompleted,
3741
- chunkRows.length,
3742
- ),
3743
- });
3857
+ lastChunkProgressAt = now;
3858
+ void updateMapProgress(
3859
+ {
3860
+ completed,
3861
+ total: progressTotalRows,
3862
+ startedAt: mapStartedAt,
3863
+ message: formatMapProgressMessage(completed, progressTotalRows),
3864
+ },
3865
+ { forceFlush: force },
3866
+ );
3867
+ };
3868
+ const reportSettledToolRequests = (count: number) => {
3869
+ if (count <= 0) return;
3870
+ reportChunkProgress(false);
3744
3871
  };
3745
3872
  // Row concurrency comes from the Governor: an explicit map concurrency is
3746
3873
  // clamped to the policy row-max, otherwise the policy default. Each row
@@ -3760,6 +3887,8 @@ function createMinimalWorkerCtx(
3760
3887
  reused?: boolean;
3761
3888
  runId?: string;
3762
3889
  completedAt?: number;
3890
+ staleAt?: number | null;
3891
+ staleAfterSeconds?: number | null;
3763
3892
  }
3764
3893
  >
3765
3894
  | undefined
@@ -3784,12 +3913,25 @@ function createMinimalWorkerCtx(
3784
3913
  const rowSlot = await governor.acquireRowSlot({
3785
3914
  signal: abortSignal,
3786
3915
  });
3916
+ let rowMarkedActive = false;
3787
3917
  try {
3918
+ startedExecutedRows += 1;
3919
+ activeExecutedRows += 1;
3920
+ rowMarkedActive = true;
3921
+ reportExecutionHeartbeat(false);
3788
3922
  const entry = uniqueRowsToExecuteEntries[myIndex]!;
3789
- const row = pendingRowsByKey.has(entry.rowKey)
3923
+ const pendingRow = pendingRowsByKey.get(entry.rowKey);
3924
+ const row = pendingRow
3790
3925
  ? ({
3791
3926
  ...entry.row,
3792
- ...publicCsvInputRow(pendingRowsByKey.get(entry.rowKey)!),
3927
+ ...publicCsvInputRow(pendingRow),
3928
+ ...(pendingRow[DEEPLINE_CELL_META_FIELD] &&
3929
+ typeof pendingRow[DEEPLINE_CELL_META_FIELD] === 'object'
3930
+ ? {
3931
+ [DEEPLINE_CELL_META_FIELD]:
3932
+ pendingRow[DEEPLINE_CELL_META_FIELD],
3933
+ }
3934
+ : {}),
3793
3935
  } as T & Record<string, unknown>)
3794
3936
  : entry.row;
3795
3937
  const absoluteIndex = entry.absoluteIndex;
@@ -3804,6 +3946,8 @@ function createMinimalWorkerCtx(
3804
3946
  reused?: boolean;
3805
3947
  runId?: string;
3806
3948
  completedAt?: number;
3949
+ staleAt?: number | null;
3950
+ staleAfterSeconds?: number | null;
3807
3951
  }
3808
3952
  > = {};
3809
3953
  const waterfallOutputs: RecordedWaterfallOutput[] = [];
@@ -3857,10 +4001,25 @@ function createMinimalWorkerCtx(
3857
4001
  ? (rawCellMeta as {
3858
4002
  status?: string;
3859
4003
  completedAt?: number;
4004
+ staleAt?: number | null;
4005
+ staleAfterSeconds?: number | null;
3860
4006
  })
3861
4007
  : null,
3862
4008
  policy: cellPolicies?.[key],
3863
4009
  });
4010
+ const previousCell = previousCellFromValue({
4011
+ hasValue: isCompletedWorkerFieldValue(enriched[key]),
4012
+ value: enriched[key],
4013
+ meta:
4014
+ rawCellMeta && typeof rawCellMeta === 'object'
4015
+ ? (rawCellMeta as {
4016
+ status?: string;
4017
+ completedAt?: number;
4018
+ staleAt?: number | null;
4019
+ staleAfterSeconds?: number | null;
4020
+ })
4021
+ : null,
4022
+ });
3864
4023
  if (reuseDecision.action === 'reuse') {
3865
4024
  cellMetaPatch[key] = {
3866
4025
  status: 'cached',
@@ -3875,6 +4034,7 @@ function createMinimalWorkerCtx(
3875
4034
  enriched,
3876
4035
  rowCtx,
3877
4036
  absoluteIndex,
4037
+ previousCell,
3878
4038
  isWorkerStepProgram(value)
3879
4039
  ? {
3880
4040
  parentField: key,
@@ -3892,11 +4052,18 @@ function createMinimalWorkerCtx(
3892
4052
  runId: req.runId,
3893
4053
  };
3894
4054
  } else {
4055
+ const completedAt = nowMs();
4056
+ const stalenessMeta = resolveCompletedCellStalenessMeta({
4057
+ policy: authoredCellPolicies?.[key],
4058
+ value: resolved.value,
4059
+ completedAt,
4060
+ });
3895
4061
  cellMetaPatch[key] = {
3896
4062
  status: 'completed',
3897
4063
  stage: key,
3898
4064
  runId: req.runId,
3899
- completedAt: nowMs(),
4065
+ completedAt,
4066
+ ...stalenessMeta,
3900
4067
  };
3901
4068
  }
3902
4069
  }
@@ -3924,7 +4091,13 @@ function createMinimalWorkerCtx(
3924
4091
  ? cellMetaPatch
3925
4092
  : undefined;
3926
4093
  executedRows[myIndex] = enriched as T & Record<string, unknown>;
4094
+ completedExecutedRows += 1;
4095
+ reportChunkProgress(false);
3927
4096
  } finally {
4097
+ if (rowMarkedActive) {
4098
+ activeExecutedRows = Math.max(0, activeExecutedRows - 1);
4099
+ reportExecutionHeartbeat(false);
4100
+ }
3928
4101
  rowSlot.release();
3929
4102
  }
3930
4103
  }
@@ -3970,7 +4143,27 @@ function createMinimalWorkerCtx(
3970
4143
  });
3971
4144
  };
3972
4145
  const workersStartedAt = nowMs();
3973
- const workerResults = await Promise.allSettled(workers);
4146
+ // Track completion with a boolean flag rather than narrowing a
4147
+ // closure-assigned `| null` variable: TypeScript's control-flow analysis
4148
+ // does not see the assignment inside `.then(...)`, so a
4149
+ // `while (results === null)` loop would narrow it to `never` afterwards.
4150
+ let workerSettled = false;
4151
+ const workerResultsPromise = Promise.allSettled(workers).then(
4152
+ (results) => {
4153
+ workerSettled = true;
4154
+ return results;
4155
+ },
4156
+ );
4157
+ while (!workerSettled) {
4158
+ await Promise.race([
4159
+ workerResultsPromise,
4160
+ sleepWorkerMs(MAP_EXECUTION_HEARTBEAT_INTERVAL_MS),
4161
+ ]);
4162
+ if (!workerSettled) {
4163
+ reportExecutionHeartbeat(false);
4164
+ }
4165
+ }
4166
+ const workerResults = await workerResultsPromise;
3974
4167
  recordRunnerPerfTrace({
3975
4168
  req,
3976
4169
  phase: 'runner.map_chunk.execute_workers',
@@ -4137,7 +4330,7 @@ function createMinimalWorkerCtx(
4137
4330
  `inserted=${totalRowsInserted} skipped=${totalRowsSkipped}`;
4138
4331
  const completedAt = nowMs();
4139
4332
  callbacks?.onMapCompleted?.(mapNodeId, completedAt);
4140
- updateMapProgress({
4333
+ void updateMapProgress({
4141
4334
  completed: totalRowsWritten,
4142
4335
  total: totalRowsWritten,
4143
4336
  completedAt,
@@ -4188,7 +4381,6 @@ function createMinimalWorkerCtx(
4188
4381
  });
4189
4382
  };
4190
4383
 
4191
- let totalRowsWritten = 0;
4192
4384
  let chunkIndex = 0;
4193
4385
  let chunkStart = 0;
4194
4386
  for await (const chunkRows of iterDatasetChunks(inputRows, rowsPerChunk)) {
@@ -4202,7 +4394,7 @@ function createMinimalWorkerCtx(
4202
4394
  totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
4203
4395
  totalRowsInserted += chunkResult.rowsInserted;
4204
4396
  totalRowsSkipped += chunkResult.rowsSkipped;
4205
- updateMapProgress({
4397
+ await updateMapProgress({
4206
4398
  completed: totalRowsWritten,
4207
4399
  total: rowCountHint ?? undefined,
4208
4400
  message: formatMapProgressMessage(
@@ -4266,14 +4458,31 @@ function createMinimalWorkerCtx(
4266
4458
  program.steps.map((step) => [step.name, step.resolver]),
4267
4459
  );
4268
4460
  const cellPolicies = Object.fromEntries(
4461
+ program.steps.map((step) => [
4462
+ step.name,
4463
+ step.staleAfterSeconds === undefined
4464
+ ? {}
4465
+ : normalizeCellStalenessPolicy({
4466
+ staleAfterSeconds: step.staleAfterSeconds,
4467
+ }),
4468
+ ]),
4469
+ ) as CellStalenessPolicyByField;
4470
+ const authoredCellPolicies = Object.fromEntries(
4269
4471
  program.steps.map((step) => [
4270
4472
  step.name,
4271
4473
  step.staleAfterSeconds === undefined
4272
4474
  ? {}
4273
4475
  : { staleAfterSeconds: step.staleAfterSeconds },
4274
4476
  ]),
4477
+ ) as AuthoredCellStalenessPolicyByField;
4478
+ return runMap(
4479
+ this.name,
4480
+ this.rows,
4481
+ fields,
4482
+ cellPolicies,
4483
+ authoredCellPolicies,
4484
+ opts,
4275
4485
  );
4276
- return runMap(this.name, this.rows, fields, cellPolicies, opts);
4277
4486
  },
4278
4487
  {
4279
4488
  emptyColumnName:
@@ -4288,7 +4497,9 @@ function createMinimalWorkerCtx(
4288
4497
 
4289
4498
  withColumn(
4290
4499
  name: string,
4291
- resolver: WorkerStepProgramStep['resolver'],
4500
+ resolver: StepProgramDatasetColumnInput<
4501
+ WorkerStepProgramStep['resolver']
4502
+ >,
4292
4503
  options?: StepProgramDatasetOptions,
4293
4504
  ): this {
4294
4505
  this.builder.withColumn(name, resolver, options);
@@ -4492,14 +4703,31 @@ function createMinimalWorkerCtx(
4492
4703
  fieldsDef.steps.map((step) => [step.name, step.resolver]),
4493
4704
  );
4494
4705
  const cellPolicies = Object.fromEntries(
4706
+ fieldsDef.steps.map((step) => [
4707
+ step.name,
4708
+ step.staleAfterSeconds === undefined
4709
+ ? {}
4710
+ : normalizeCellStalenessPolicy({
4711
+ staleAfterSeconds: step.staleAfterSeconds,
4712
+ }),
4713
+ ]),
4714
+ ) as CellStalenessPolicyByField;
4715
+ const authoredCellPolicies = Object.fromEntries(
4495
4716
  fieldsDef.steps.map((step) => [
4496
4717
  step.name,
4497
4718
  step.staleAfterSeconds === undefined
4498
4719
  ? {}
4499
4720
  : { staleAfterSeconds: step.staleAfterSeconds },
4500
4721
  ]),
4722
+ ) as AuthoredCellStalenessPolicyByField;
4723
+ return runMap(
4724
+ name,
4725
+ rows,
4726
+ fields,
4727
+ cellPolicies,
4728
+ authoredCellPolicies,
4729
+ opts,
4501
4730
  );
4502
- return runMap(name, rows, fields, cellPolicies, opts);
4503
4731
  }
4504
4732
  throw new Error(
4505
4733
  'ctx.dataset(key, rows, fields, options) is not supported. Use ctx.dataset(key, rows).withColumn(...).run(options).',
@@ -5277,6 +5505,10 @@ async function executeRunRequest(
5277
5505
  ];
5278
5506
  let lastLedgerFlushAt = startedAt;
5279
5507
  let ledgerFlushInFlight: Promise<void> = Promise.resolve();
5508
+ let ledgerFlushQueueDepth = 0;
5509
+ let lastCoordinatorProgressPublishAt = 0;
5510
+ let coordinatorProgressPublishInFlight: Promise<void> = Promise.resolve();
5511
+ let coordinatorProgressPublishQueueDepth = 0;
5280
5512
 
5281
5513
  const appendRunLogLine = (line: string) => {
5282
5514
  const trimmed = redactSecretsFromLogString(line.trim());
@@ -5399,6 +5631,36 @@ async function executeRunRequest(
5399
5631
  });
5400
5632
  };
5401
5633
 
5634
+ const flushCoordinatorProgressEvent = (force: boolean): Promise<void> => {
5635
+ const now = nowMs();
5636
+ if (
5637
+ !force &&
5638
+ now - lastCoordinatorProgressPublishAt <
5639
+ MAP_EXECUTION_HEARTBEAT_INTERVAL_MS
5640
+ ) {
5641
+ return Promise.resolve();
5642
+ }
5643
+ if (!force && coordinatorProgressPublishQueueDepth > 0) {
5644
+ return Promise.resolve();
5645
+ }
5646
+ lastCoordinatorProgressPublishAt = now;
5647
+ coordinatorProgressPublishQueueDepth += 1;
5648
+ coordinatorProgressPublishInFlight = coordinatorProgressPublishInFlight
5649
+ .catch(() => undefined)
5650
+ .then(async () => {
5651
+ try {
5652
+ await publishCoordinatorProgressEvent(now);
5653
+ } finally {
5654
+ coordinatorProgressPublishQueueDepth = Math.max(
5655
+ 0,
5656
+ coordinatorProgressPublishQueueDepth - 1,
5657
+ );
5658
+ }
5659
+ })
5660
+ .catch(() => undefined);
5661
+ return force ? coordinatorProgressPublishInFlight : Promise.resolve();
5662
+ };
5663
+
5402
5664
  const appendStepLifecycleEvent = (event: PlayStepLifecycleEvent) => {
5403
5665
  updateStepProgress({
5404
5666
  nodeId: event.nodeId,
@@ -5498,15 +5760,19 @@ async function executeRunRequest(
5498
5760
  return events;
5499
5761
  };
5500
5762
 
5501
- const flushLedgerEvents = (force: boolean): void => {
5502
- if (!options?.persistResultDatasets) return;
5763
+ const flushLedgerEvents = (force: boolean): Promise<void> => {
5764
+ if (!options?.persistResultDatasets) return Promise.resolve();
5503
5765
  const now = nowMs();
5504
5766
  if (!force && now - lastLedgerFlushAt < RUN_LEDGER_FLUSH_INTERVAL_MS) {
5505
- return;
5767
+ return Promise.resolve();
5768
+ }
5769
+ if (!force && ledgerFlushQueueDepth > 0) {
5770
+ return Promise.resolve();
5506
5771
  }
5507
5772
  const events = drainPendingLedgerEvents(now);
5508
- if (events.length === 0) return;
5773
+ if (events.length === 0) return Promise.resolve();
5509
5774
  lastLedgerFlushAt = now;
5775
+ ledgerFlushQueueDepth += 1;
5510
5776
  ledgerFlushInFlight = ledgerFlushInFlight
5511
5777
  .catch(() => undefined)
5512
5778
  .then(async () => {
@@ -5519,10 +5785,12 @@ async function executeRunRequest(
5519
5785
  } catch {
5520
5786
  pendingLedgerEvents = [...events, ...pendingLedgerEvents];
5521
5787
  throw new Error('runtime run-ledger append failed');
5788
+ } finally {
5789
+ ledgerFlushQueueDepth = Math.max(0, ledgerFlushQueueDepth - 1);
5522
5790
  }
5523
- await publishCoordinatorProgressEvent(now).catch(() => undefined);
5524
5791
  })
5525
5792
  .catch(() => undefined);
5793
+ return force ? ledgerFlushInFlight : Promise.resolve();
5526
5794
  };
5527
5795
 
5528
5796
  const flushTerminalLedgerEvents = async (
@@ -5564,7 +5832,12 @@ async function executeRunRequest(
5564
5832
  const workerCallbacks: WorkerCtxCallbacks = {
5565
5833
  onNodeProgress: (input) => {
5566
5834
  updateStepProgress(input);
5567
- flushLedgerEvents(Boolean(input.forceFlush));
5835
+ const force = Boolean(input.forceFlush);
5836
+ const ledgerFlush = flushLedgerEvents(force);
5837
+ const progressFlush = flushCoordinatorProgressEvent(force);
5838
+ return force
5839
+ ? Promise.all([ledgerFlush, progressFlush]).then(() => undefined)
5840
+ : Promise.resolve();
5568
5841
  },
5569
5842
  onMapStarted: (nodeId, at) => stepLifecycle?.onMapStarted(nodeId, at),
5570
5843
  onMapCompleted: (nodeId, at) => stepLifecycle?.onMapCompleted(nodeId, at),