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
@@ -68,6 +68,7 @@ import {
68
68
  } from './child-play-await';
69
69
  import { submitChildPlayThroughCoordinator } from './child-play-submit';
70
70
  import { isRetryableWorkerRuntimeApiError } from './runtime-api-retry';
71
+ import { formatCustomerConsoleValue } from './runtime/customer-console';
71
72
  import {
72
73
  attachToolResultListDataset,
73
74
  createToolExecuteResult,
@@ -116,6 +117,7 @@ import {
116
117
  isQueryResultDatasetTool,
117
118
  } from '../../../shared_libs/play-runtime/query-result-dataset';
118
119
  import { DEDUPE_DUPLICATE_KEY_SAMPLE_CAP } from '../../../shared_libs/play-runtime/map-row-identity';
120
+ import { createRuntimeDatasetId } from '../../../shared_libs/play-runtime/dataset-id';
119
121
  import {
120
122
  getTopLevelPipelineSubsteps,
121
123
  getCompiledPipelineSubsteps,
@@ -196,12 +198,12 @@ import {
196
198
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
197
199
  runtimeLeaseHeartbeatIntervalMs,
198
200
  } from '../../../shared_libs/play-runtime/lease-policy';
199
- import { RuntimeSheetRowsBlockedError } from '../../../shared_libs/play-runtime/runtime-sheet-errors';
200
201
  import {
201
202
  resolveRuntimeMapRowAdmission,
202
203
  runtimeMapJsonByteLength,
203
204
  } from '../../../shared_libs/play-runtime/map-memory-limits';
204
205
  import { createSerialTaskQueue } from '../../../shared_libs/play-runtime/serial-task-queue';
206
+ import { resolveChildExecutionPlacement as resolveChildExecutionStrategy } from '../../../shared_libs/play-runtime/child-execution-placement';
205
207
  // The harness stub forwards leaf calls (validation, runtime-api HTTP) into
206
208
  // the long-lived Play Harness Worker via env.HARNESS. We import the
207
209
  // `setHarnessBinding` setter eagerly so it's available the moment
@@ -214,12 +216,10 @@ import { createSerialTaskQueue } from '../../../shared_libs/play-runtime/serial-
214
216
  // re-bundle harness internals into per-play. Keep that in mind.
215
217
  import {
216
218
  harnessPersistCompletedSheetRows,
217
- harnessHeartbeatSheetAttempt,
218
219
  harnessReadSheetDatasetRowKeys,
219
220
  harnessReadSheetDatasetRows,
220
221
  harnessReadStagedFileChunk,
221
222
  harnessReleaseRuntimeReceipts,
222
- harnessReleaseSheetAttempt,
223
223
  harnessStartSheetDataset,
224
224
  setHarnessBinding,
225
225
  } from '../../../sdk/src/plays/harness-stub';
@@ -300,7 +300,7 @@ import type {
300
300
  LiveNodeProgressMap,
301
301
  LiveNodeProgressSnapshot,
302
302
  } from './runtime/live-progress';
303
- import { extractErrorBilling } from './runtime/tool-http-errors';
303
+ import { extractErrorBilling, ToolHttpError } from './runtime/tool-http-errors';
304
304
  import {
305
305
  WorkflowAbortError,
306
306
  isAbortLikeError,
@@ -324,6 +324,55 @@ import type { PlayRunInputPayload } from '../../../shared_libs/play-runtime/sche
324
324
  // @ts-ignore - virtual module substituted by esbuild plugin at bundle time.
325
325
  import playFn from 'deepline-play-entry';
326
326
 
327
+ const customerConsoleStorage = new AsyncLocalStorage<
328
+ (message: string) => void
329
+ >();
330
+
331
+ type CustomerConsoleGlobal = typeof globalThis & {
332
+ __deeplineCaptureCustomerConsole?: (
333
+ method: string,
334
+ args: unknown[],
335
+ ) => unknown;
336
+ };
337
+
338
+ (globalThis as CustomerConsoleGlobal).__deeplineCaptureCustomerConsole = (
339
+ method,
340
+ args,
341
+ ) => {
342
+ const log = customerConsoleStorage.getStore();
343
+ if (log) {
344
+ assertNoSecretTaint(args, `console.${method}`);
345
+ const message = args.map(formatCustomerConsoleValue).join(' ');
346
+ log(`[console.${method}] ${message}`.slice(0, 2_000));
347
+ }
348
+ };
349
+
350
+ function runCustomerPlay(
351
+ ctx: unknown,
352
+ input: PlayRunInputPayload,
353
+ ): Promise<unknown> {
354
+ return customerConsoleStorage.run(
355
+ (ctx as { log(message: string): void }).log,
356
+ () => (playFn as typeof runCustomerPlay)(ctx, input),
357
+ );
358
+ }
359
+
360
+ function runtimeDatasetIdentity(playName: string, tableNamespace: string) {
361
+ return {
362
+ datasetId: createRuntimeDatasetId(playName, tableNamespace),
363
+ path: `datasets.${tableNamespace}`,
364
+ tableNamespace,
365
+ };
366
+ }
367
+
368
+ function datasetAvailableLog(
369
+ runId: string,
370
+ dataset: ReturnType<typeof runtimeDatasetIdentity>,
371
+ count: number,
372
+ ): string {
373
+ return `Dataset ${dataset.path} available: ${count} persisted rows; export: deepline runs export ${runId} --dataset ${dataset.path} --out ${dataset.tableNamespace}.csv`;
374
+ }
375
+
327
376
  type RunRequest = {
328
377
  runId: string;
329
378
  callbackUrl: string;
@@ -999,6 +1048,17 @@ type WorkerCtxCallbacks = {
999
1048
  }) => void | Promise<void>;
1000
1049
  onMapStarted?: (nodeId: string, at?: number) => void;
1001
1050
  onMapCompleted?: (nodeId: string, at?: number) => void;
1051
+ onDatasetLifecycle?: (event: {
1052
+ datasetId: string;
1053
+ path: string;
1054
+ tableNamespace: string;
1055
+ phase: 'registered' | 'available' | 'failed';
1056
+ persistedRows?: number;
1057
+ succeededRows?: number;
1058
+ failedRows?: number;
1059
+ complete?: boolean;
1060
+ at: number;
1061
+ }) => void | Promise<void>;
1002
1062
  onToolCalled?: (toolId: string, at?: number) => void;
1003
1063
  onToolFailed?: (toolId: string, at?: number) => void;
1004
1064
  };
@@ -1201,136 +1261,6 @@ function makeRuntimeSheetAttempt(input: {
1201
1261
  };
1202
1262
  }
1203
1263
 
1204
- class RuntimeSheetAttemptLeaseLostError extends Error {
1205
- readonly rowKeys: string[];
1206
- readonly runId: string;
1207
- readonly attemptId: string;
1208
- readonly tableNamespace: string;
1209
-
1210
- constructor(input: {
1211
- rowKeys: string[];
1212
- runId: string;
1213
- attemptId: string;
1214
- tableNamespace: string;
1215
- cause?: unknown;
1216
- }) {
1217
- const rowSummary =
1218
- input.rowKeys.length === 1
1219
- ? input.rowKeys[0]!
1220
- : `${input.rowKeys.length} rows`;
1221
- super(
1222
- `Runtime sheet attempt ${input.attemptId} lost ownership of ${rowSummary} in ${input.tableNamespace}.`,
1223
- { cause: input.cause },
1224
- );
1225
- this.name = 'RuntimeSheetAttemptLeaseLostError';
1226
- this.rowKeys = input.rowKeys;
1227
- this.runId = input.runId;
1228
- this.attemptId = input.attemptId;
1229
- this.tableNamespace = input.tableNamespace;
1230
- }
1231
- }
1232
-
1233
- async function executeWithRuntimeSheetAttemptHeartbeat<T>(input: {
1234
- req: RunRequest;
1235
- tableNamespace: string;
1236
- sheetContract: PlaySheetContract;
1237
- attemptId?: string | null;
1238
- attemptOwnerRunId?: string | null;
1239
- attemptSeq?: number | null;
1240
- rowKeys: string[];
1241
- execute: () => Promise<T> | T;
1242
- }): Promise<T> {
1243
- const attemptId = input.attemptId?.trim();
1244
- const rowKeys = [
1245
- ...new Set(input.rowKeys.map((key) => key.trim()).filter(Boolean)),
1246
- ];
1247
- if (!attemptId || rowKeys.length === 0) {
1248
- return await input.execute();
1249
- }
1250
- const heartbeatIntervalMs = runtimePolicyHeartbeatIntervalMs(
1251
- input.req,
1252
- runtimeSheetAttemptLeaseTtlMs(input.req) ??
1253
- PLAY_RUNTIME_SHEET_ATTEMPT_LEASE_TTL_MS,
1254
- );
1255
-
1256
- let stopped = false;
1257
- let timeout: ReturnType<typeof setTimeout> | null = null;
1258
- let rejectLeaseLost!: (error: RuntimeSheetAttemptLeaseLostError) => void;
1259
- const leaseLost = new Promise<never>((_, reject) => {
1260
- rejectLeaseLost = reject;
1261
- });
1262
-
1263
- const stop = () => {
1264
- stopped = true;
1265
- if (timeout !== null) {
1266
- clearTimeout(timeout);
1267
- timeout = null;
1268
- }
1269
- };
1270
-
1271
- const heartbeatOnce = async () => {
1272
- const result = await harnessHeartbeatSheetAttempt({
1273
- ...runtimeSheetSessionScope(input.req),
1274
- tableNamespace: input.tableNamespace,
1275
- sheetContract: input.sheetContract,
1276
- runId: input.req.runId,
1277
- attemptId,
1278
- attemptOwnerRunId: input.attemptOwnerRunId ?? input.req.runId,
1279
- attemptSeq: input.attemptSeq ?? input.req.runAttempt ?? 0,
1280
- attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(input.req),
1281
- keys: rowKeys,
1282
- });
1283
- const renewed = new Set(result.renewedKeys);
1284
- const lostKeys = rowKeys.filter((key) => !renewed.has(key));
1285
- if (lostKeys.length > 0) {
1286
- throw new RuntimeSheetAttemptLeaseLostError({
1287
- rowKeys: lostKeys,
1288
- runId: input.req.runId,
1289
- attemptId,
1290
- tableNamespace: input.tableNamespace,
1291
- });
1292
- }
1293
- };
1294
-
1295
- const schedule = () => {
1296
- timeout = setTimeout(() => {
1297
- void (async () => {
1298
- try {
1299
- await heartbeatOnce();
1300
- } catch (error) {
1301
- stop();
1302
- if (error instanceof RuntimeSheetAttemptLeaseLostError) {
1303
- rejectLeaseLost(error);
1304
- } else {
1305
- rejectLeaseLost(
1306
- new RuntimeSheetAttemptLeaseLostError({
1307
- rowKeys,
1308
- runId: input.req.runId,
1309
- attemptId,
1310
- tableNamespace: input.tableNamespace,
1311
- cause: error,
1312
- }),
1313
- );
1314
- }
1315
- return;
1316
- }
1317
- if (!stopped) {
1318
- schedule();
1319
- }
1320
- })();
1321
- }, heartbeatIntervalMs);
1322
- };
1323
-
1324
- schedule();
1325
- const execution = Promise.resolve().then(input.execute);
1326
- execution.catch(() => {});
1327
- try {
1328
- return await Promise.race([execution, leaseLost]);
1329
- } finally {
1330
- stop();
1331
- }
1332
- }
1333
-
1334
1264
  /**
1335
1265
  * Strip credentials and JWT-shaped tokens from any string before it lands in
1336
1266
  * a log buffer or upstream error message. The harness routinely echoes
@@ -2073,6 +2003,7 @@ async function callToolDirect(
2073
2003
  while (true) {
2074
2004
  requestAttempt += 1;
2075
2005
  let res: Response;
2006
+ const providerCallStartedAt = nowMs();
2076
2007
  try {
2077
2008
  const runtimeApiTimeout = resolveToolRuntimeApiTimeout({
2078
2009
  requestInput: input,
@@ -2161,8 +2092,11 @@ async function callToolDirect(
2161
2092
  } catch (error) {
2162
2093
  transportAttempt += 1;
2163
2094
  const message = error instanceof Error ? error.message : String(error);
2164
- lastError = new Error(
2095
+ lastError = new ToolHttpError(
2165
2096
  `Tool ${toolId} transport failed calling ${path} for run ${req.runId} on attempt ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
2097
+ null,
2098
+ 0,
2099
+ 'repairable',
2166
2100
  );
2167
2101
  if (
2168
2102
  transportAttempt >= TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS ||
@@ -2212,6 +2146,7 @@ async function callToolDirect(
2212
2146
  bodyText: text,
2213
2147
  retryAfterHeader: res.headers.get('retry-after'),
2214
2148
  transientHttpRetrySafe,
2149
+ providerLatencyMs: nowMs() - providerCallStartedAt,
2215
2150
  });
2216
2151
  lastError = failure.error;
2217
2152
  if (failure.backpressureDelayMs !== null) {
@@ -2458,10 +2393,7 @@ function isRunFatalWorkerRowError(error: unknown): boolean {
2458
2393
  isRowIsolationExemptError(error) ||
2459
2394
  isRuntimeReceiptPersistenceError(error) ||
2460
2395
  isRuntimePersistenceCircuitOpenError(error) ||
2461
- error instanceof RuntimeSheetAttemptLeaseLostError ||
2462
- error instanceof RuntimeLeaseLostError ||
2463
- (error instanceof Error &&
2464
- error.name === 'RuntimeSheetAttemptLeaseLostError')
2396
+ error instanceof RuntimeLeaseLostError
2465
2397
  );
2466
2398
  }
2467
2399
 
@@ -3541,53 +3473,18 @@ function resolveSheetContractFromReq(
3541
3473
  }
3542
3474
  }
3543
3475
 
3544
- /**
3545
- * Release attempt-scoped leases after terminal settlement. Completed and failed
3546
- * rows are durable; retaining their live fences only delays the next repair run.
3547
- */
3476
+ /** Release temporary work-receipt ownership after terminal settlement. */
3548
3477
  async function releaseRuntimeLeasesOnSettlement(input: {
3549
3478
  req: RunRequest;
3550
3479
  leasedSheetNamespaces: Set<string>;
3551
3480
  emit: (event: RunnerEvent) => void;
3552
3481
  reason: 'completed' | 'run-fatal';
3553
3482
  }): Promise<void> {
3554
- const { req, leasedSheetNamespaces, emit, reason } = input;
3483
+ const { req, emit, reason } = input;
3484
+ void input.leasedSheetNamespaces;
3555
3485
  const reasonLabel =
3556
3486
  reason === 'completed' ? 'completed settlement' : 'run-fatal teardown';
3557
3487
  const scope = runtimeSheetSessionScope(req);
3558
- for (const tableNamespace of leasedSheetNamespaces) {
3559
- const sheetContract = resolveSheetContractFromReq(req, tableNamespace);
3560
- if (!sheetContract) continue;
3561
- try {
3562
- const released = await harnessReleaseSheetAttempt({
3563
- ...scope,
3564
- tableNamespace,
3565
- sheetContract,
3566
- runId: req.runId,
3567
- attemptOwnerRunId: req.runId,
3568
- attemptSeq: req.runAttempt ?? 0,
3569
- });
3570
- if (released.released > 0) {
3571
- emit({
3572
- type: 'log',
3573
- level: 'info',
3574
- message: `Released ${released.released} runtime sheet row lease(s) for ctx.dataset("${tableNamespace}") on ${reasonLabel}.`,
3575
- ts: nowMs(),
3576
- });
3577
- }
3578
- } catch (releaseError) {
3579
- emit({
3580
- type: 'log',
3581
- level: 'warn',
3582
- message: `Runtime sheet attempt release failed for ctx.dataset("${tableNamespace}") on ${reasonLabel} (rerun will fall back to lease-TTL expiry): ${
3583
- releaseError instanceof Error
3584
- ? releaseError.message
3585
- : String(releaseError)
3586
- }`,
3587
- ts: nowMs(),
3588
- });
3589
- }
3590
- }
3591
3488
  try {
3592
3489
  const released = await harnessReleaseRuntimeReceipts({
3593
3490
  ...scope,
@@ -3662,6 +3559,7 @@ async function persistCompletedMapRows(input: {
3662
3559
  attemptOwnerRunId?: string | null;
3663
3560
  attemptExpiresAt?: string | null;
3664
3561
  attemptSeq?: number | null;
3562
+ writeVersion?: number | null;
3665
3563
  budgetMeter?: WorkerWorkBudgetMeter;
3666
3564
  force?: boolean;
3667
3565
  forceFailedRows?: boolean;
@@ -3671,6 +3569,8 @@ async function persistCompletedMapRows(input: {
3671
3569
  visible: number;
3672
3570
  retryWritten: number | null;
3673
3571
  retryVisible: number | null;
3572
+ staleDropped: number;
3573
+ staleDroppedKeys: string[];
3674
3574
  }> {
3675
3575
  if (input.rows.length === 0) {
3676
3576
  return {
@@ -3679,6 +3579,8 @@ async function persistCompletedMapRows(input: {
3679
3579
  visible: 0,
3680
3580
  retryWritten: null,
3681
3581
  retryVisible: null,
3582
+ staleDropped: 0,
3583
+ staleDroppedKeys: [],
3682
3584
  };
3683
3585
  }
3684
3586
  const { req, tableNamespace } = input;
@@ -3714,6 +3616,7 @@ async function persistCompletedMapRows(input: {
3714
3616
  attemptOwnerRunId: input.attemptOwnerRunId ?? null,
3715
3617
  attemptExpiresAt: input.attemptExpiresAt ?? null,
3716
3618
  attemptSeq: input.attemptSeq ?? req.runAttempt ?? 0,
3619
+ writeVersion: input.writeVersion ?? null,
3717
3620
  forceFailedRows: input.forceFailedRows === true,
3718
3621
  };
3719
3622
  input.budgetMeter?.count('sheet');
@@ -3747,24 +3650,8 @@ async function persistCompletedMapRows(input: {
3747
3650
  return result.rows.length;
3748
3651
  };
3749
3652
  const result = await harnessPersistCompletedSheetRows(persistRequest);
3750
- const hasAttemptFence = Boolean(input.attemptId?.trim());
3751
- const assertAttemptStillOwnsTerminalWrites = (
3752
- written: number,
3753
- visible: number | null,
3754
- ) => {
3755
- // Recovery finalization may include rows completed by an earlier attempt.
3756
- // A complete terminal readback proves they were not lost.
3757
- if (
3758
- !hasAttemptFence ||
3759
- written === rows.length ||
3760
- (visible !== null && visible >= expectedVisibleRows)
3761
- ) {
3762
- return;
3763
- }
3764
- throw new Error(
3765
- `Runtime sheet attempt lost ownership for ${tableNamespace}: wrote ${written}/${rows.length}; run ${req.runId}; attempt ${input.attemptId}.`,
3766
- );
3767
- };
3653
+ const staleDropped = result.staleDropped ?? 0;
3654
+ const staleDroppedKeys = result.staleDroppedKeys ?? result.fencedKeys ?? [];
3768
3655
  console.warn('[play-runner.persist_completed_map_rows.result]', {
3769
3656
  runId: req.runId,
3770
3657
  playName: req.playName,
@@ -3793,6 +3680,7 @@ async function persistCompletedMapRows(input: {
3793
3680
  attemptOwnerRunId: input.attemptOwnerRunId ?? null,
3794
3681
  attemptExpiresAt: input.attemptExpiresAt ?? null,
3795
3682
  attemptSeq: input.attemptSeq ?? req.runAttempt ?? 0,
3683
+ writeVersion: input.writeVersion ?? null,
3796
3684
  attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(req),
3797
3685
  inputOffset: 0,
3798
3686
  });
@@ -3801,7 +3689,6 @@ async function persistCompletedMapRows(input: {
3801
3689
  input.budgetMeter?.count('sheet');
3802
3690
  retryWritten = retry.rowsWritten;
3803
3691
  retryVisible = await readVisibleRowCount();
3804
- assertAttemptStillOwnsTerminalWrites(retryWritten, retryVisible);
3805
3692
  if (retryVisible >= expectedVisibleRows) {
3806
3693
  return {
3807
3694
  rows: rows.length,
@@ -3809,13 +3696,19 @@ async function persistCompletedMapRows(input: {
3809
3696
  visible: visibleRows,
3810
3697
  retryWritten,
3811
3698
  retryVisible,
3699
+ staleDropped: staleDropped + (retry.staleDropped ?? 0),
3700
+ staleDroppedKeys: [
3701
+ ...new Set([
3702
+ ...staleDroppedKeys,
3703
+ ...(retry.staleDroppedKeys ?? retry.fencedKeys ?? []),
3704
+ ]),
3705
+ ],
3812
3706
  };
3813
3707
  }
3814
3708
  throw new Error(
3815
3709
  `Runtime sheet persistence mismatch for ${tableNamespace}: wrote ${result.rowsWritten}/${rows.length}; visible ${visibleRows}/${expectedVisibleRows}; retry wrote ${retryWritten}/${rows.length}; retry visible ${retryVisible}/${expectedVisibleRows}; run ${req.runId}.`,
3816
3710
  );
3817
3711
  }
3818
- assertAttemptStillOwnsTerminalWrites(result.rowsWritten, visibleRows);
3819
3712
  }
3820
3713
  if (result.rowsWritten !== rows.length && visibleRows < expectedVisibleRows) {
3821
3714
  throw new Error(
@@ -3828,6 +3721,8 @@ async function persistCompletedMapRows(input: {
3828
3721
  visible: visibleRows,
3829
3722
  retryWritten,
3830
3723
  retryVisible,
3724
+ staleDropped,
3725
+ staleDroppedKeys,
3831
3726
  };
3832
3727
  }
3833
3728
 
@@ -3841,6 +3736,7 @@ async function applyLiveMapRowUpdates(input: {
3841
3736
  attemptOwnerRunId?: string | null;
3842
3737
  attemptExpiresAt?: string | null;
3843
3738
  attemptSeq?: number | null;
3739
+ writeVersion?: number | null;
3844
3740
  budgetMeter?: WorkerWorkBudgetMeter;
3845
3741
  }): Promise<void> {
3846
3742
  if (input.updates.length === 0) return;
@@ -3891,6 +3787,7 @@ async function prepareMapRows(input: {
3891
3787
  attemptOwnerRunId?: string | null;
3892
3788
  attemptExpiresAt?: string | null;
3893
3789
  attemptSeq?: number | null;
3790
+ writeVersion?: number | null;
3894
3791
  budgetMeter?: WorkerWorkBudgetMeter;
3895
3792
  force?: boolean;
3896
3793
  }): Promise<{
@@ -3903,6 +3800,7 @@ async function prepareMapRows(input: {
3903
3800
  attemptOwnerRunId?: string | null;
3904
3801
  attemptExpiresAt?: string | null;
3905
3802
  attemptSeq?: number | null;
3803
+ writeVersion?: number | null;
3906
3804
  }> {
3907
3805
  if (input.rows.length === 0) {
3908
3806
  return {
@@ -3915,6 +3813,7 @@ async function prepareMapRows(input: {
3915
3813
  attemptOwnerRunId: input.attemptOwnerRunId ?? null,
3916
3814
  attemptExpiresAt: input.attemptExpiresAt ?? null,
3917
3815
  attemptSeq: input.attemptSeq ?? input.req.runAttempt ?? 0,
3816
+ writeVersion: input.writeVersion ?? null,
3918
3817
  };
3919
3818
  }
3920
3819
  const sessionScope = runtimeSheetSessionScope(input.req);
@@ -3934,6 +3833,7 @@ async function prepareMapRows(input: {
3934
3833
  attemptOwnerRunId: input.attemptOwnerRunId ?? null,
3935
3834
  attemptExpiresAt: input.attemptExpiresAt ?? null,
3936
3835
  attemptSeq: input.attemptSeq ?? input.req.runAttempt ?? 0,
3836
+ writeVersion: input.writeVersion ?? null,
3937
3837
  attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(input.req),
3938
3838
  ...(input.force ? { force: true } : {}),
3939
3839
  });
@@ -3971,6 +3871,7 @@ async function prepareMapRows(input: {
3971
3871
  attemptExpiresAt: result.attemptExpiresAt ?? input.attemptExpiresAt ?? null,
3972
3872
  attemptSeq:
3973
3873
  result.attemptSeq ?? input.attemptSeq ?? input.req.runAttempt ?? 0,
3874
+ writeVersion: result.writeVersion ?? input.writeVersion ?? null,
3974
3875
  };
3975
3876
  }
3976
3877
 
@@ -4024,18 +3925,6 @@ function childPipelineUsesCtxDataset(
4024
3925
  });
4025
3926
  }
4026
3927
 
4027
- function childPipelineNeedsWorkflowScheduler(
4028
- pipeline: PlayStaticPipeline | null | undefined,
4029
- ): boolean {
4030
- if (!pipeline) return false;
4031
- return flattenStaticPipeline(pipeline).some(
4032
- (substep) =>
4033
- substep.type === 'tool' &&
4034
- (substep.isEventWait === true ||
4035
- substep.toolId === 'test_wait_for_event'),
4036
- );
4037
- }
4038
-
4039
3928
  /**
4040
3929
  * Build the per-(org,provider) rate port the distributed Rate State Backend
4041
3930
  * RPCs through. When the coordinator binding (or its rate RPCs) is absent we
@@ -4864,7 +4753,75 @@ function createMinimalWorkerCtx(
4864
4753
  return kept;
4865
4754
  };
4866
4755
 
4756
+ // Reserve the whole logical map before dispatching any provider work. One
4757
+ // schedule-time version represents the user's intent across every chunk;
4758
+ // a later run can supersede it without waiting, and an older chunk can
4759
+ // never allocate a newer version halfway through the map.
4760
+ const inputCapabilities = datasetHandleCapabilities(inputRows);
4761
+ if (!inputCapabilities.replayable) {
4762
+ throw new Error(
4763
+ `ctx.dataset("${name}") requires a replayable input so all row write intents can be reserved before execution.`,
4764
+ );
4765
+ }
4766
+ const reservationRows: Record<string, unknown>[] = [];
4767
+ const reservationExplicitKeys =
4768
+ opts?.key === undefined ? null : new Set<string>();
4769
+ let reservationOffset = 0;
4770
+ let reservationChunkIndex = 0;
4771
+ for await (const chunk of iterDatasetChunks(inputRows, rowsPerChunk)) {
4772
+ assertNotAborted(abortSignal);
4773
+ for (let localIndex = 0; localIndex < chunk.length; localIndex += 1) {
4774
+ const row = chunk[localIndex]!;
4775
+ const absoluteIndex = baseOffset + reservationOffset + localIndex;
4776
+ const explicitKeyValue = resolveExplicitKeyValue(row, absoluteIndex);
4777
+ if (explicitKeyValue !== null && reservationExplicitKeys) {
4778
+ if (reservationExplicitKeys.has(explicitKeyValue)) continue;
4779
+ reservationExplicitKeys.add(explicitKeyValue);
4780
+ }
4781
+ const rowKey = resolveRowKey(
4782
+ row,
4783
+ absoluteIndex,
4784
+ reservationChunkIndex,
4785
+ localIndex,
4786
+ );
4787
+ reservationRows.push({
4788
+ ...runtimeCsvStorageRow(row),
4789
+ ...mapRowOutcomeRuntimeFields({
4790
+ key: rowKey,
4791
+ inputIndex: absoluteIndex,
4792
+ }),
4793
+ });
4794
+ }
4795
+ reservationOffset += chunk.length;
4796
+ reservationChunkIndex += 1;
4797
+ }
4798
+ const mapWriteAttempt = makeRuntimeSheetAttempt({
4799
+ runId: req.runId,
4800
+ runAttempt: req.runAttempt,
4801
+ mapName: name,
4802
+ chunkIndex: 'finalize',
4803
+ });
4804
+ const mapReservation =
4805
+ reservationRows.length === 0
4806
+ ? null
4807
+ : await harnessStartSheetDataset({
4808
+ ...runtimeSheetSessionScope(req),
4809
+ tableNamespace: name,
4810
+ sheetContract: augmentSheetContractWithDatasetFields({
4811
+ contract: requireSheetContract(req, name),
4812
+ rows: reservationRows,
4813
+ outputFields,
4814
+ }),
4815
+ rows: reservationRows,
4816
+ runId: req.runId,
4817
+ inputOffset: 0,
4818
+ attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(req),
4819
+ ...mapWriteAttempt,
4820
+ });
4821
+ const mapWriteVersion = mapReservation?.writeVersion ?? null;
4822
+
4867
4823
  let totalRowsWritten = 0;
4824
+ let datasetLifecycleRegistered = false;
4868
4825
 
4869
4826
  const volatileWorkflowChunkRows = new Map<
4870
4827
  number,
@@ -4912,9 +4869,26 @@ function createMinimalWorkerCtx(
4912
4869
  })),
4913
4870
  inputOffset: baseOffset + chunkStart,
4914
4871
  ...rowAttempt,
4872
+ writeVersion: mapWriteVersion,
4915
4873
  budgetMeter: workBudgetMeter,
4916
4874
  force: !!req.force,
4917
4875
  });
4876
+ if (!datasetLifecycleRegistered) {
4877
+ datasetLifecycleRegistered = true;
4878
+ const dataset = runtimeDatasetIdentity(req.playName, name);
4879
+ await callbacks?.onDatasetLifecycle?.({
4880
+ ...dataset,
4881
+ phase: 'registered',
4882
+ complete: false,
4883
+ at: nowMs(),
4884
+ });
4885
+ emitEvent({
4886
+ type: 'log',
4887
+ level: 'info',
4888
+ message: `Dataset ${dataset.path} registered`,
4889
+ ts: nowMs(),
4890
+ });
4891
+ }
4918
4892
  const activeRowAttempt = {
4919
4893
  attemptId: prepared.attemptId ?? rowAttempt.attemptId,
4920
4894
  attemptOwnerRunId:
@@ -4922,10 +4896,8 @@ function createMinimalWorkerCtx(
4922
4896
  attemptExpiresAt:
4923
4897
  prepared.attemptExpiresAt ?? rowAttempt.attemptExpiresAt,
4924
4898
  attemptSeq: prepared.attemptSeq ?? rowAttempt.attemptSeq,
4899
+ writeVersion: prepared.writeVersion ?? null,
4925
4900
  };
4926
- // Record that this run holds attempt leases in `name`'s sheet so a
4927
- // run-fatal teardown releases them (the table now exists).
4928
- leasedSheetNamespaces?.add(name);
4929
4901
  recordRunnerPerfTrace({
4930
4902
  req,
4931
4903
  phase: 'runner.map_chunk.prepare_rows',
@@ -4961,8 +4933,6 @@ function createMinimalWorkerCtx(
4961
4933
  const pendingRowsByKey = new Map<string, Record<string, unknown>>();
4962
4934
  const completedKeys = new Set<string>();
4963
4935
  const completedRowsByKey = new Map<string, Record<string, unknown>>();
4964
- const blockedKeys = new Set<string>();
4965
- const blockedRowsByKey = new Map<string, Record<string, unknown>>();
4966
4936
  const preparedKeys = new Set<string>();
4967
4937
  for (const row of prepared.pendingRows) {
4968
4938
  const key =
@@ -4989,66 +4959,12 @@ function createMinimalWorkerCtx(
4989
4959
  resolveMapRowOutcomeKey(row) ??
4990
4960
  deriveDefaultMapRowIdentity(row, resolveRuntimeInputIndex(row) ?? 0);
4991
4961
  if (key) {
4992
- blockedKeys.add(key);
4993
- blockedRowsByKey.set(key, row);
4994
4962
  preparedKeys.add(key);
4995
4963
  }
4996
4964
  }
4997
- if (blockedKeys.size > 0) {
4998
- const rowKeySamples = Array.from(blockedKeys).slice(
4999
- 0,
5000
- MAP_ROW_FAILURE_SAMPLE_LIMIT,
5001
- );
5002
- const message =
5003
- `ctx.dataset("${name}") chunk ${chunkIndex} is blocked by ` +
5004
- `${blockedKeys.size} active runtime sheet row lease(s) owned by another attempt. ` +
5005
- `Retry after the owning attempt finishes or the row leases expire.` +
5006
- (rowKeySamples.length > 0
5007
- ? ` Blocked row keys: ${rowKeySamples.join(', ')}`
5008
- : '');
5009
- recordRunnerPerfTrace({
5010
- req,
5011
- phase: 'runner.map_chunk.blocked_rows',
5012
- ms: nowMs() - prepareStartedAt,
5013
- extra: {
5014
- mapName: name,
5015
- chunkIndex,
5016
- rowsBlocked: blockedKeys.size,
5017
- rowKeySamples,
5018
- },
5019
- });
5020
- enqueueMapProgressUpdate({
5021
- completed: preparedCompletedRows,
5022
- total: progressTotalRows,
5023
- waitingRows: blockedKeys.size,
5024
- startedAt: mapStartedAt,
5025
- message,
5026
- });
5027
- return {
5028
- status: 'blocked',
5029
- chunkIndex,
5030
- rangeStart: baseOffset + chunkStart,
5031
- rangeEnd: baseOffset + chunkStart + chunkRows.length,
5032
- rowsRead: chunkRows.length,
5033
- rowsBlocked: blockedKeys.size,
5034
- rowKeySamples,
5035
- blockedRowSamples: rowKeySamples.map((rowKey) => {
5036
- const blockedRow = blockedRowsByKey.get(rowKey);
5037
- const attemptId =
5038
- typeof blockedRow?.__deeplineAttemptId === 'string'
5039
- ? blockedRow.__deeplineAttemptId
5040
- : typeof blockedRow?._attempt_id === 'string'
5041
- ? blockedRow._attempt_id
5042
- : activeRowAttempt.attemptId;
5043
- return {
5044
- __deeplineRowKey: rowKey,
5045
- __deeplineAttemptId: attemptId,
5046
- };
5047
- }),
5048
- outputDatasetId: `map:${name}`,
5049
- message,
5050
- };
5051
- }
4965
+ // Versioned scheduling never waits for row ownership. Compatibility
4966
+ // adapters may still report an old `blockedRows` disposition; treat it
4967
+ // as a stale/non-executable row and keep it out of this run's output.
5052
4968
  const missingPreparedRows = chunkEntries.filter(
5053
4969
  ({ rowKey }) => !preparedKeys.has(rowKey),
5054
4970
  );
@@ -5375,12 +5291,29 @@ function createMinimalWorkerCtx(
5375
5291
  ts: nowMs(),
5376
5292
  });
5377
5293
  }
5294
+ const staleKeys = new Set(persistStats.staleDroppedKeys);
5378
5295
  for (const { executedIndex } of rowsToPersist) {
5379
- persistedExecutedIndexes.add(executedIndex);
5296
+ const rowKey = uniqueRowsToExecuteEntries[executedIndex]!.rowKey;
5297
+ if (staleKeys.has(rowKey)) continue;
5298
+ if (!persistedExecutedIndexes.has(executedIndex)) {
5299
+ persistedExecutedIndexes.add(executedIndex);
5300
+ completedExecutedRows += 1;
5301
+ }
5380
5302
  }
5381
5303
  for (const { executedIndex } of failedRowsToPersist) {
5304
+ const rowKey = uniqueRowsToExecuteEntries[executedIndex]!.rowKey;
5305
+ if (staleKeys.has(rowKey)) continue;
5382
5306
  persistedFailedIndexes.add(executedIndex);
5383
5307
  }
5308
+ if (persistStats.staleDropped > 0) {
5309
+ emitEvent({
5310
+ type: 'log',
5311
+ level: 'info',
5312
+ message: `Dropped ${persistStats.staleDropped} stale runtime sheet write(s) for ctx.dataset("${name}"); a newer scheduled row version is visible.`,
5313
+ ts: nowMs(),
5314
+ });
5315
+ }
5316
+ reportChunkProgress(false);
5384
5317
  };
5385
5318
 
5386
5319
  const enqueuePersistExecutedRows = (): Promise<void> => {
@@ -5678,7 +5611,6 @@ function createMinimalWorkerCtx(
5678
5611
  : undefined;
5679
5612
  executedRows[myIndex] = enriched as T &
5680
5613
  Record<string, unknown>;
5681
- completedExecutedRows += 1;
5682
5614
  await notePersistableRow(enriched);
5683
5615
  reportChunkProgress(false);
5684
5616
  } catch (rowError) {
@@ -5788,25 +5720,14 @@ function createMinimalWorkerCtx(
5788
5720
  return results;
5789
5721
  },
5790
5722
  );
5791
- const workerResults = await executeWithRuntimeSheetAttemptHeartbeat({
5792
- req,
5793
- tableNamespace: name,
5794
- sheetContract: requireSheetContract(req, name),
5795
- ...activeRowAttempt,
5796
- rowKeys: uniqueRowsToExecuteEntries.map((entry) => entry.rowKey),
5797
- execute: async () => {
5798
- while (!workerSettled) {
5799
- await Promise.race([
5800
- workerResultsPromise,
5801
- sleepWorkerMs(MAP_EXECUTION_HEARTBEAT_INTERVAL_MS),
5802
- ]);
5803
- if (!workerSettled) {
5804
- reportExecutionHeartbeat(false);
5805
- }
5806
- }
5807
- return await workerResultsPromise;
5808
- },
5809
- });
5723
+ while (!workerSettled) {
5724
+ await Promise.race([
5725
+ workerResultsPromise,
5726
+ sleepWorkerMs(MAP_EXECUTION_HEARTBEAT_INTERVAL_MS),
5727
+ ]);
5728
+ if (!workerSettled) reportExecutionHeartbeat(false);
5729
+ }
5730
+ const workerResults = await workerResultsPromise;
5810
5731
  recordRunnerPerfTrace({
5811
5732
  req,
5812
5733
  phase: 'runner.map_chunk.execute_workers',
@@ -6004,6 +5925,7 @@ function createMinimalWorkerCtx(
6004
5925
  executedIndex < executedRows.length;
6005
5926
  executedIndex += 1
6006
5927
  ) {
5928
+ if (!persistedExecutedIndexes.has(executedIndex)) continue;
6007
5929
  const executedRow = executedRows[executedIndex];
6008
5930
  const key = uniqueRowsToExecuteEntries[executedIndex]!.rowKey;
6009
5931
  // Failed rows have no executed result; they stay out of the map output
@@ -6039,6 +5961,8 @@ function createMinimalWorkerCtx(
6039
5961
  ...mapRowOutcomeRuntimeFields({ key, inputIndex }),
6040
5962
  }));
6041
5963
  const completedRowsReused = completedChunkEntries.length;
5964
+ const committedExecutedRows =
5965
+ persistedExecutedIndexes.size + persistedFailedIndexes.size;
6042
5966
  const hashStartedAt = nowMs();
6043
5967
  const hash = await hashJson(publicOut);
6044
5968
  const includeCachedRowsInChunkResult = !workflowStep;
@@ -6068,8 +5992,8 @@ function createMinimalWorkerCtx(
6068
5992
  chunkIndex,
6069
5993
  rowsRead: chunkRows.length,
6070
5994
  rowsWritten: out.length,
6071
- rowsExecuted: executedRows.length,
6072
- rowsFailed: failedExecutedRows,
5995
+ rowsExecuted: committedExecutedRows,
5996
+ rowsFailed: persistedFailedIndexes.size,
6073
5997
  rowsCached: completedRowsReused,
6074
5998
  },
6075
5999
  });
@@ -6080,12 +6004,12 @@ function createMinimalWorkerCtx(
6080
6004
  rangeEnd: baseOffset + chunkStart + out.length,
6081
6005
  rowsRead: chunkRows.length,
6082
6006
  rowsWritten: out.length,
6083
- rowsExecuted: executedRows.length,
6007
+ rowsExecuted: committedExecutedRows,
6084
6008
  rowsCached: completedRowsReused,
6085
6009
  rowsDuplicateReused: duplicateInputReuseCount,
6086
6010
  rowsInserted,
6087
6011
  rowsSkipped,
6088
- rowsFailed: failedExecutedRows,
6012
+ rowsFailed: persistedFailedIndexes.size,
6089
6013
  rowFailureSamples,
6090
6014
  stepCellsCompleted,
6091
6015
  stepCellsSkipped,
@@ -6117,12 +6041,9 @@ function createMinimalWorkerCtx(
6117
6041
  let totalRowsInserted = 0;
6118
6042
  let totalRowsSkipped = 0;
6119
6043
  let totalRowsFailed = 0;
6120
- let totalRowsBlocked = 0;
6121
6044
  let totalStepCellsCompleted = 0;
6122
6045
  let totalStepCellsSkipped = 0;
6123
6046
  const totalRowFailureSamples: Array<{ rowKey: string; error: string }> = [];
6124
- const blockedRowKeySamples: string[] = [];
6125
- const blockedRowSamples: Record<string, unknown>[] = [];
6126
6047
 
6127
6048
  const runChunkStep = async (
6128
6049
  chunkRows: T[],
@@ -6261,6 +6182,23 @@ function createMinimalWorkerCtx(
6261
6182
  finalizedRowsWritten,
6262
6183
  ),
6263
6184
  });
6185
+ const dataset = runtimeDatasetIdentity(req.playName, name);
6186
+ const persistedRows = finalizedRowsWritten + totalRowsFailed;
6187
+ await callbacks?.onDatasetLifecycle?.({
6188
+ ...dataset,
6189
+ phase: 'available',
6190
+ persistedRows,
6191
+ succeededRows: finalizedRowsWritten,
6192
+ failedRows: totalRowsFailed,
6193
+ complete: true,
6194
+ at: completedAt,
6195
+ });
6196
+ emitEvent({
6197
+ type: 'log',
6198
+ level: totalRowsFailed > 0 ? 'warn' : 'info',
6199
+ message: datasetAvailableLog(req.runId, dataset, persistedRows),
6200
+ ts: completedAt,
6201
+ });
6264
6202
  callbacks?.onMapCompleted?.(mapNodeId, completedAt);
6265
6203
  emitEvent({
6266
6204
  type: 'log',
@@ -6299,6 +6237,7 @@ function createMinimalWorkerCtx(
6299
6237
  runId: req.runId,
6300
6238
  inputOffset: 0,
6301
6239
  attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(req),
6240
+ writeVersion: mapWriteVersion,
6302
6241
  ...repairAttempt,
6303
6242
  });
6304
6243
  const activeRepairAttempt = {
@@ -6308,6 +6247,7 @@ function createMinimalWorkerCtx(
6308
6247
  attemptExpiresAt:
6309
6248
  repairStart.attemptExpiresAt ?? repairAttempt.attemptExpiresAt,
6310
6249
  attemptSeq: repairStart.attemptSeq ?? repairAttempt.attemptSeq,
6250
+ writeVersion: repairStart.writeVersion ?? null,
6311
6251
  };
6312
6252
  const repair = await persistCompletedMapRows({
6313
6253
  req,
@@ -6509,32 +6449,12 @@ function createMinimalWorkerCtx(
6509
6449
  );
6510
6450
  },
6511
6451
  onBatchComplete: async (chunkResult) => {
6512
- if (chunkResult.status === 'blocked') {
6513
- totalRowsBlocked += chunkResult.rowsBlocked;
6514
- for (const key of chunkResult.rowKeySamples) {
6515
- if (blockedRowKeySamples.length >= MAP_ROW_FAILURE_SAMPLE_LIMIT) {
6516
- break;
6517
- }
6518
- blockedRowKeySamples.push(key);
6519
- }
6520
- for (const row of chunkResult.blockedRowSamples) {
6521
- if (blockedRowSamples.length >= MAP_ROW_FAILURE_SAMPLE_LIMIT) {
6522
- break;
6523
- }
6524
- blockedRowSamples.push(row);
6525
- }
6526
- emitEvent({
6527
- type: 'log',
6528
- level: 'warn',
6529
- message: chunkResult.message,
6530
- ts: nowMs(),
6531
- });
6532
- return true;
6533
- }
6452
+ if (chunkResult.status === 'blocked') return true;
6534
6453
  if (chunkResult.fatalError) {
6535
6454
  throw new Error(
6536
6455
  `ctx.dataset("${name}") stopped after a runtime persistence failure ` +
6537
- `outside the retryable chunk step. Provider calls already executed for ` +
6456
+ `outside the retryable chunk step. Circuit breaker prevented queued row ` +
6457
+ `dispatch. Provider calls already executed for ` +
6538
6458
  `${chunkResult.rowsExecuted} row(s), so the chunk was not retried. ` +
6539
6459
  `Fix the runtime persistence cause and re-run to resume. ` +
6540
6460
  `First error: ${chunkResult.fatalError}`,
@@ -6621,13 +6541,6 @@ function createMinimalWorkerCtx(
6621
6541
  ts: nowMs(),
6622
6542
  });
6623
6543
  }
6624
- if (totalRowsBlocked > 0) {
6625
- await flushQueuedMapProgressUpdates();
6626
- throw new RuntimeSheetRowsBlockedError({
6627
- tableNamespace: name,
6628
- blockedRows: blockedRowSamples,
6629
- });
6630
- }
6631
6544
  if (failFastRowErrors && totalRowsFailed > 0 && totalRowsWritten > 0) {
6632
6545
  // onRowError:'fail', PARTIAL failure (some rows committed): fail the run
6633
6546
  // without finalizing the dataset. The committed rows already persisted
@@ -6992,6 +6905,7 @@ function createMinimalWorkerCtx(
6992
6905
  options?: {
6993
6906
  description?: string;
6994
6907
  timeoutMs?: number;
6908
+ execution?: 'inline' | 'child-workflow';
6995
6909
  },
6996
6910
  ): Promise<unknown> {
6997
6911
  const normalizedKey = normalizeContextKey(key, 'runPlay');
@@ -7066,8 +6980,35 @@ function createMinimalWorkerCtx(
7066
6980
  const childIsDatasetBacked = childPipelineUsesCtxDataset(
7067
6981
  childManifest.staticPipeline,
7068
6982
  );
6983
+ const childWaitsForEvent = (
6984
+ childManifest.staticPipeline
6985
+ ? flattenStaticPipeline(childManifest.staticPipeline)
6986
+ : []
6987
+ ).some(
6988
+ (step) =>
6989
+ step.type === 'tool' &&
6990
+ (step.isEventWait || step.toolId === 'test_wait_for_event'),
6991
+ );
6992
+ const authoredWorkflow =
6993
+ options?.timeoutMs != null ||
6994
+ childIsDatasetBacked ||
6995
+ childWaitsForEvent;
6996
+ if (options?.execution === 'inline' && authoredWorkflow) {
6997
+ throw new Error(
6998
+ `ctx.runPlay("${resolvedName}") requested inline execution for a child that requires a workflow.`,
6999
+ );
7000
+ }
7001
+ const childExecutionDecision = resolveChildExecutionStrategy({
7002
+ requiresWorkflow: authoredWorkflow,
7003
+ execution: options?.execution,
7004
+ });
7069
7005
  const childNeedsWorkflowScheduler =
7070
- childPipelineNeedsWorkflowScheduler(childManifest.staticPipeline);
7006
+ childExecutionDecision.strategy === 'scheduled';
7007
+ if (rowScope && childNeedsWorkflowScheduler) {
7008
+ throw new Error(
7009
+ `ctx.runPlay("${resolvedName}") cannot start a child workflow per dataset row.`,
7010
+ );
7011
+ }
7071
7012
  console.info('[play.runtime.span]', {
7072
7013
  event: 'play.runtime.span',
7073
7014
  phase: 'child_route',
@@ -7116,8 +7057,7 @@ function createMinimalWorkerCtx(
7116
7057
  coordinatorBinding: cachedCoordinatorBinding,
7117
7058
  makeRequestId,
7118
7059
  coordinatorRequestHeaders,
7119
- allowInline:
7120
- options?.timeoutMs == null && !childNeedsWorkflowScheduler,
7060
+ allowInline: childExecutionDecision.strategy === 'inline',
7121
7061
  body: {
7122
7062
  name: resolvedName,
7123
7063
  childIdempotencyKey: childInvocationId,
@@ -7607,9 +7547,7 @@ async function handleRun(request: Request, env: WorkerEnv): Promise<Response> {
7607
7547
  captureHarnessBinding(env);
7608
7548
  await probeHarnessOnce(env, runPrefix);
7609
7549
  const ctx = createMinimalWorkerCtx(req, emit, env);
7610
- const result = await (
7611
- playFn as (ctx: unknown, input: PlayRunInputPayload) => Promise<unknown>
7612
- )(ctx, req.runtimeInput);
7550
+ const result = await runCustomerPlay(ctx, req.runtimeInput);
7613
7551
  emit({
7614
7552
  type: 'result',
7615
7553
  result,
@@ -8023,6 +7961,17 @@ async function executeRunRequest(
8023
7961
  },
8024
7962
  onMapStarted: (nodeId, at) => stepLifecycle?.onMapStarted(nodeId, at),
8025
7963
  onMapCompleted: (nodeId, at) => stepLifecycle?.onMapCompleted(nodeId, at),
7964
+ onDatasetLifecycle: async (event) => {
7965
+ const { at, ...dataset } = event;
7966
+ pendingLedgerEvents.push({
7967
+ ...dataset,
7968
+ type: 'dataset.lifecycle',
7969
+ runId: req.runId,
7970
+ source: 'worker',
7971
+ occurredAt: at,
7972
+ });
7973
+ await flushLedgerEvents(event.complete === true);
7974
+ },
8026
7975
  onToolCalled: (toolId, at) => stepLifecycle?.onToolCalled(toolId, at),
8027
7976
  onToolFailed: (toolId, at) => stepLifecycle?.onToolFailed(toolId, at),
8028
7977
  };
@@ -8089,9 +8038,7 @@ async function executeRunRequest(
8089
8038
  : null;
8090
8039
  try {
8091
8040
  const playStartedAt = nowMs();
8092
- const result = await (
8093
- playFn as (ctx: unknown, input: PlayRunInputPayload) => Promise<unknown>
8094
- )(ctx, req.runtimeInput);
8041
+ const result = await runCustomerPlay(ctx, req.runtimeInput);
8095
8042
  recordRunnerPerfTrace({
8096
8043
  req,
8097
8044
  phase: 'runner.play_function',
@@ -8135,7 +8082,33 @@ async function executeRunRequest(
8135
8082
  ms: nowMs() - ledgerFlushWaitStartedAt,
8136
8083
  });
8137
8084
  const resultDatasetStartedAt = nowMs();
8138
- await persistResultDatasets(req, promoted.handles, serializedResult);
8085
+ const persistedResultCounts = await persistResultDatasets(
8086
+ req,
8087
+ promoted.handles,
8088
+ serializedResult,
8089
+ );
8090
+ for (const dataset of promoted.handles) {
8091
+ if (dataset.datasetKind === 'map') continue;
8092
+ const identity = runtimeDatasetIdentity(
8093
+ req.playName,
8094
+ dataset.tableNamespace,
8095
+ );
8096
+ const count = persistedResultCounts.get(dataset.tableNamespace) ?? 0;
8097
+ pendingLedgerEvents.push({
8098
+ ...identity,
8099
+ type: 'dataset.lifecycle',
8100
+ runId: req.runId,
8101
+ source: 'worker',
8102
+ occurredAt: nowMs(),
8103
+ phase: 'available',
8104
+ persistedRows: count,
8105
+ succeededRows: count,
8106
+ failedRows: 0,
8107
+ complete: true,
8108
+ });
8109
+ appendRunLogLine(datasetAvailableLog(req.runId, identity, count));
8110
+ }
8111
+ await flushLedgerEvents(true);
8139
8112
  resultDatasetsPersisted = true;
8140
8113
  recordRunnerPerfTrace({
8141
8114
  req,
@@ -8599,8 +8572,9 @@ async function persistResultDatasets(
8599
8572
  req: RunRequest,
8600
8573
  resultDatasets: ProjectedResultDatasetHandle[],
8601
8574
  serializedResult: unknown,
8602
- ): Promise<void> {
8575
+ ): Promise<Map<string, number>> {
8603
8576
  const persistedNamespaces = new Set<string>();
8577
+ const persistedCounts = new Map<string, number>();
8604
8578
  for (const dataset of resultDatasets) {
8605
8579
  if (dataset.datasetKind === 'map') continue;
8606
8580
  const handle = dataset.handle as WorkerDatasetHandle<
@@ -8639,6 +8613,7 @@ async function persistResultDatasets(
8639
8613
  inputOffset += chunk.length;
8640
8614
  }
8641
8615
  persistedNamespaces.add(dataset.tableNamespace);
8616
+ persistedCounts.set(dataset.tableNamespace, inputOffset);
8642
8617
  }
8643
8618
 
8644
8619
  const datasets = collectDatasetEnvelopes(serializedResult);
@@ -8672,7 +8647,9 @@ async function persistResultDatasets(
8672
8647
  attemptLeaseTtlMs: runtimeSheetAttemptLeaseTtlMs(req),
8673
8648
  userEmail: req.userEmail,
8674
8649
  });
8650
+ persistedCounts.set(dataset.tableNamespace, dataset.rows.length);
8675
8651
  }
8652
+ return persistedCounts;
8676
8653
  }
8677
8654
 
8678
8655
  const RESULT_DATASET_PERSIST_CHUNK_ROWS = 5_000;