deepline 0.1.182 → 0.1.184

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 (69) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
  2. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
  3. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +639 -109
  4. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
  5. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1752 -1899
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
  7. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
  8. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
  9. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
  10. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
  11. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
  12. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
  13. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
  14. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
  15. package/dist/bundling-sources/sdk/src/client.ts +41 -0
  16. package/dist/bundling-sources/sdk/src/http.ts +23 -8
  17. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
  18. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  19. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +133 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1612 -287
  26. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
  32. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
  36. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
  40. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
  42. package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
  43. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
  44. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +21 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2038 -262
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
  52. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
  53. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
  58. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
  59. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +28 -11
  60. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +38 -6
  61. package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
  62. package/dist/cli/index.js +76 -17
  63. package/dist/cli/index.mjs +76 -17
  64. package/dist/index.d.mts +2 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.js +63 -9
  67. package/dist/index.mjs +63 -9
  68. package/dist/plays/bundle-play-file.mjs +20 -6
  69. package/package.json +1 -1
@@ -22,7 +22,10 @@ import {
22
22
  type WorkflowRunner,
23
23
  } from '@cloudflare/dynamic-workflows';
24
24
  import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
25
+ import { buildChildRunId as buildSharedChildRunId } from '../../../shared_libs/play-runtime/child-run-id';
25
26
  import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
27
+ import type { RuntimeTestPolicyOverrides } from '../../../shared_libs/play-runtime/test-runtime-seams';
28
+ import { DYNAMIC_PLAY_WORKER_ARTIFACT_VERSION } from '../../../shared_libs/play-runtime/dynamic-worker-version';
26
29
  import {
27
30
  DB_SESSION_DEFAULT_TTL_SECONDS,
28
31
  createDbSessionResponseSchema,
@@ -54,6 +57,15 @@ import {
54
57
  COORDINATOR_INTERNAL_TOKEN_HEADER,
55
58
  COORDINATOR_RUN_SCOPE_HEADER,
56
59
  } from '../../../shared_libs/play-runtime/coordinator-headers';
60
+ import {
61
+ PLAY_RUNTIME_CONTRACT,
62
+ PLAY_RUNTIME_CONTRACT_HEADER,
63
+ } from '../../../shared_libs/play-runtime/runtime-contract';
64
+ import {
65
+ PLAY_RUNTIME_API_COMPAT_PATH,
66
+ PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
67
+ PLAY_RUNTIME_TAIL_LOG_COMPAT_PATH,
68
+ } from '../../../shared_libs/play-runtime/runtime-api-paths';
57
69
  import {
58
70
  decideWorkflowPlatformRetry,
59
71
  PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
@@ -85,6 +97,8 @@ export type PlayWorkflowParams = {
85
97
  artifactHash: string;
86
98
  graphHash: string;
87
99
  input: Record<string, unknown>;
100
+ force?: boolean;
101
+ runAttempt?: number;
88
102
  inputFile?: {
89
103
  name?: string;
90
104
  r2Key?: string;
@@ -126,6 +140,8 @@ export type PlayWorkflowParams = {
126
140
  totalRows?: number;
127
141
  coordinatorUrl?: string | null;
128
142
  coordinatorInternalToken?: string | null;
143
+ runtimeTestFaultHeader?: string | null;
144
+ testPolicyOverrides?: RuntimeTestPolicyOverrides | null;
129
145
  submittedAt?: number | null;
130
146
  };
131
147
 
@@ -649,6 +665,9 @@ const SUBMIT_INITIAL_STATE_POLL_MS = 50;
649
665
  const WORKFLOW_RETRY_STATE_TTL_MS = 60 * 60 * 1000;
650
666
  const WORKFLOW_SUBMIT_DUPLICATE_WAIT_MS = 55_000;
651
667
  const WORKFLOW_SUBMIT_DUPLICATE_POLL_MS = 250;
668
+ const CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS = [
669
+ 250, 750, 1500, 3000, 5000, 10000,
670
+ ] as const;
652
671
  const COORDINATOR_WORKER_MEMORY_LIMIT_MESSAGE =
653
672
  'Worker memory limit hit before the run could finish. For CSV enrich, use --output or --in-place for automatic batches; otherwise rerun smaller --rows ranges.';
654
673
 
@@ -668,7 +687,7 @@ function buildDynamicWorkflowMetadata(
668
687
  graphHash: params.graphHash,
669
688
  artifactStorageKey: params.artifactStorageKey,
670
689
  artifactHash: params.artifactHash ?? null,
671
- dynamicWorkerCode: params.dynamicWorkerCode ?? null,
690
+ dynamicWorkerCode: null,
672
691
  integrationMode: normalizeIntegrationMode(params.integrationMode),
673
692
  packagedFiles: normalizePackagedFiles(params.packagedFiles),
674
693
  };
@@ -746,10 +765,11 @@ async function createDynamicWorkflowInstance(input: {
746
765
  env: CoordinatorEnv;
747
766
  id: string;
748
767
  params: PlayWorkflowParams;
768
+ metadataParams?: PlayWorkflowParams;
749
769
  }): Promise<WorkflowInstance> {
750
770
  const workflow = createDispatcherWorkflowBinding(
751
771
  () => input.env.PLAY_WORKFLOW as unknown as Workflow,
752
- buildDynamicWorkflowMetadata(input.params),
772
+ buildDynamicWorkflowMetadata(input.metadataParams ?? input.params),
753
773
  );
754
774
  return workflow.create({
755
775
  id: input.id,
@@ -1127,6 +1147,96 @@ async function hydrateWorkflowDbSessions(input: {
1127
1147
  };
1128
1148
  }
1129
1149
 
1150
+ async function hydrateWorkflowLaunchParams(input: {
1151
+ env: CoordinatorEnv;
1152
+ event: unknown;
1153
+ trace: CoordinatorPerfTraceSink;
1154
+ }): Promise<unknown> {
1155
+ if (!isRecord(input.event)) return input.event;
1156
+ const payload = isRecord(input.event.payload) ? input.event.payload : null;
1157
+ const envelopeParams = isRecord(payload?.params) ? payload.params : null;
1158
+ const params = envelopeParams ?? payload;
1159
+ if (
1160
+ !payload ||
1161
+ !params ||
1162
+ (!isRecord(params.workflowParams) && !isRecord(params.workflowParamsRef))
1163
+ ) {
1164
+ return input.event;
1165
+ }
1166
+ const startedAt = Date.now();
1167
+ const hydrated = await hydrateWorkflowRetryParams({
1168
+ env: input.env,
1169
+ params: params.workflowParams,
1170
+ paramsRef: params.workflowParamsRef,
1171
+ });
1172
+ if (!hydrated) return input.event;
1173
+ const submittedAt =
1174
+ typeof params.submittedAt === 'number' &&
1175
+ Number.isFinite(params.submittedAt)
1176
+ ? params.submittedAt
1177
+ : hydrated.submittedAt;
1178
+ const hydratedParams = {
1179
+ ...hydrated,
1180
+ submittedAt,
1181
+ };
1182
+ input.trace({
1183
+ runId:
1184
+ typeof hydratedParams.runId === 'string'
1185
+ ? hydratedParams.runId
1186
+ : 'unknown-workflow-run',
1187
+ phase: 'coordinator.workflow_launch_params_hydrated',
1188
+ ms: Date.now() - startedAt,
1189
+ graphHash:
1190
+ typeof hydratedParams.graphHash === 'string'
1191
+ ? hydratedParams.graphHash
1192
+ : null,
1193
+ extra: {
1194
+ paramsBytes: isRecord(params.workflowParamsRef)
1195
+ ? typeof params.workflowParamsRef.bytes === 'number'
1196
+ ? params.workflowParamsRef.bytes
1197
+ : null
1198
+ : jsonByteLength(params.workflowParams),
1199
+ paramsStorage: isRecord(params.workflowParamsRef) ? 'r2' : 'inline',
1200
+ },
1201
+ });
1202
+ return {
1203
+ ...input.event,
1204
+ payload: envelopeParams
1205
+ ? {
1206
+ ...payload,
1207
+ params: hydratedParams,
1208
+ }
1209
+ : {
1210
+ ...payload,
1211
+ ...hydratedParams,
1212
+ },
1213
+ };
1214
+ }
1215
+
1216
+ function workflowCreateParams(
1217
+ params: PlayWorkflowParams,
1218
+ launchState: {
1219
+ params?: PlayWorkflowParams;
1220
+ paramsRef?: WorkflowRetryParamsRef;
1221
+ },
1222
+ ): PlayWorkflowParams {
1223
+ const launchParams = launchState.params ?? params;
1224
+ const envelope: Record<string, unknown> = {
1225
+ runId: params.runId,
1226
+ playName: params.playName,
1227
+ graphHash: params.graphHash,
1228
+ artifactStorageKey: params.artifactStorageKey,
1229
+ artifactHash: params.artifactHash,
1230
+ submittedAt: params.submittedAt,
1231
+ };
1232
+ if (launchState.paramsRef) {
1233
+ envelope.workflowParamsRef = launchState.paramsRef;
1234
+ } else {
1235
+ envelope.workflowParams = launchParams;
1236
+ }
1237
+ return envelope as unknown as PlayWorkflowParams;
1238
+ }
1239
+
1130
1240
  function sleep(ms: number): Promise<void> {
1131
1241
  return new Promise((resolve) => setTimeout(resolve, ms));
1132
1242
  }
@@ -1209,7 +1319,7 @@ async function markWorkflowRuntimeFailure(input: {
1209
1319
  } satisfies PlayRunLedgerEvent,
1210
1320
  ],
1211
1321
  });
1212
- const url = `${baseUrl.replace(/\/$/, '')}/api/v2/plays/internal/runtime`;
1322
+ const url = `${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`;
1213
1323
  const backoffMs = [200, 500, 1500];
1214
1324
  let lastError: unknown = null;
1215
1325
  for (let attempt = 0; attempt <= backoffMs.length; attempt += 1) {
@@ -1240,6 +1350,47 @@ async function markWorkflowRuntimeFailure(input: {
1240
1350
  });
1241
1351
  }
1242
1352
 
1353
+ async function appendWorkflowChildCompletedRunEvent(input: {
1354
+ env: CoordinatorEnv;
1355
+ event: unknown;
1356
+ result: unknown;
1357
+ }): Promise<boolean> {
1358
+ const payload = readWorkflowPayload(input.event);
1359
+ if (!payload || !isRecord(payload.playCallGovernance)) return false;
1360
+ const runId = typeof payload.runId === 'string' ? payload.runId : null;
1361
+ const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl : null;
1362
+ const executorToken =
1363
+ typeof payload.executorToken === 'string' ? payload.executorToken : null;
1364
+ if (!runId || !baseUrl || !executorToken) return false;
1365
+ const headers = new Headers({
1366
+ authorization: `Bearer ${executorToken}`,
1367
+ 'content-type': 'application/json',
1368
+ });
1369
+ const bypass = input.env.VERCEL_PROTECTION_BYPASS_TOKEN?.trim();
1370
+ if (bypass) headers.set('x-vercel-protection-bypass', bypass);
1371
+ const body = JSON.stringify({
1372
+ action: 'append_run_events',
1373
+ playId: runId,
1374
+ events: [
1375
+ {
1376
+ type: 'run.completed',
1377
+ runId,
1378
+ source: 'worker',
1379
+ occurredAt: Date.now(),
1380
+ result: input.result,
1381
+ } satisfies PlayRunLedgerEvent,
1382
+ ],
1383
+ });
1384
+ const url = `${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`;
1385
+ const response = await fetch(url, { method: 'POST', headers, body });
1386
+ if (!response.ok) {
1387
+ throw new Error(
1388
+ `runtime API responded ${response.status}: ${(await response.text().catch(() => '')).slice(0, 400)}`,
1389
+ );
1390
+ }
1391
+ return true;
1392
+ }
1393
+
1243
1394
  type StoredPlayArtifactPayload = {
1244
1395
  artifact?: {
1245
1396
  bundledCode?: string;
@@ -1253,7 +1404,10 @@ async function persistWorkflowRetryState(input: {
1253
1404
  env: CoordinatorEnv;
1254
1405
  runId: string;
1255
1406
  params: PlayWorkflowParams;
1256
- }): Promise<void> {
1407
+ }): Promise<{
1408
+ params?: PlayWorkflowParams;
1409
+ paramsRef?: WorkflowRetryParamsRef;
1410
+ }> {
1257
1411
  const retryParams = buildWorkflowRetryParams(input.params);
1258
1412
  const paramsBytes = jsonByteLength(retryParams);
1259
1413
  if (paramsBytes > WORKFLOW_RETRY_PARAMS_MAX_BYTES) {
@@ -1275,21 +1429,32 @@ async function persistWorkflowRetryState(input: {
1275
1429
  runId: input.runId,
1276
1430
  hash,
1277
1431
  });
1432
+ const paramsRef: WorkflowRetryParamsRef = {
1433
+ storageKind: 'r2',
1434
+ storageKey,
1435
+ bytes: paramsBytes,
1436
+ hash,
1437
+ expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
1438
+ };
1278
1439
  await input.env.PLAYS_BUCKET.put(storageKey, serialized, {
1279
1440
  httpMetadata: { contentType: 'application/json' },
1280
1441
  });
1281
1442
  body = {
1282
1443
  runId: input.runId,
1283
- paramsRef: {
1284
- storageKind: 'r2',
1285
- storageKey,
1286
- bytes: paramsBytes,
1287
- hash,
1288
- expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
1289
- },
1444
+ paramsRef,
1290
1445
  paramsBytes,
1291
1446
  ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
1292
1447
  };
1448
+ await callRunScopedControl<{ ok?: unknown }>(
1449
+ input.env,
1450
+ input.runId,
1451
+ '/run-retry-state-put',
1452
+ {
1453
+ method: 'POST',
1454
+ body: JSON.stringify(body),
1455
+ },
1456
+ );
1457
+ return { paramsRef };
1293
1458
  } else {
1294
1459
  body = {
1295
1460
  runId: input.runId,
@@ -1307,6 +1472,7 @@ async function persistWorkflowRetryState(input: {
1307
1472
  body: JSON.stringify(body),
1308
1473
  },
1309
1474
  );
1475
+ return { params: retryParams };
1310
1476
  }
1311
1477
 
1312
1478
  async function persistWorkflowLaunchState(input: {
@@ -1318,14 +1484,16 @@ async function persistWorkflowLaunchState(input: {
1318
1484
  retryExpiresAt?: number;
1319
1485
  dbSessionsExpiresAt?: number;
1320
1486
  sessionCount?: number;
1487
+ params?: PlayWorkflowParams;
1488
+ paramsRef?: WorkflowRetryParamsRef;
1321
1489
  }> {
1322
1490
  if (input.sessions.length === 0) {
1323
- await persistWorkflowRetryState({
1491
+ const state = await persistWorkflowRetryState({
1324
1492
  env: input.env,
1325
1493
  runId: input.runId,
1326
1494
  params: input.params,
1327
1495
  });
1328
- return {};
1496
+ return state;
1329
1497
  }
1330
1498
  const retryParams = buildWorkflowRetryParams(input.params);
1331
1499
  const paramsBytes = jsonByteLength(retryParams);
@@ -1345,18 +1513,19 @@ async function persistWorkflowLaunchState(input: {
1345
1513
  runId: input.runId,
1346
1514
  hash,
1347
1515
  });
1516
+ const paramsRef: WorkflowRetryParamsRef = {
1517
+ storageKind: 'r2',
1518
+ storageKey,
1519
+ bytes: paramsBytes,
1520
+ hash,
1521
+ expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
1522
+ };
1348
1523
  await input.env.PLAYS_BUCKET.put(storageKey, serialized, {
1349
1524
  httpMetadata: { contentType: 'application/json' },
1350
1525
  });
1351
1526
  body = {
1352
1527
  runId: input.runId,
1353
- paramsRef: {
1354
- storageKind: 'r2',
1355
- storageKey,
1356
- bytes: paramsBytes,
1357
- hash,
1358
- expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
1359
- },
1528
+ paramsRef,
1360
1529
  paramsBytes,
1361
1530
  sessions: input.sessions,
1362
1531
  retryTtlMs: WORKFLOW_RETRY_STATE_TTL_MS,
@@ -1394,24 +1563,22 @@ async function persistWorkflowLaunchState(input: {
1394
1563
  typeof response.sessionCount === 'number'
1395
1564
  ? response.sessionCount
1396
1565
  : undefined,
1566
+ params: body.params,
1567
+ paramsRef: body.paramsRef,
1397
1568
  };
1398
1569
  }
1399
1570
 
1400
- async function hydrateWorkflowRetryParams(input: {
1401
- env: CoordinatorEnv;
1402
- params: unknown;
1403
- paramsRef: unknown;
1404
- }): Promise<PlayWorkflowParams | null> {
1405
- if (isRecord(input.params)) {
1406
- return input.params as PlayWorkflowParams;
1407
- }
1408
- if (!isRecord(input.paramsRef)) {
1571
+ function parseWorkflowRetryParamsRef(
1572
+ paramsRef: unknown,
1573
+ ): WorkflowRetryParamsRef | null {
1574
+ if (!isRecord(paramsRef)) {
1409
1575
  return null;
1410
1576
  }
1411
- const storageKind = input.paramsRef.storageKind;
1412
- const storageKey = input.paramsRef.storageKey;
1413
- const expectedBytes = input.paramsRef.bytes;
1414
- const expectedHash = input.paramsRef.hash;
1577
+ const storageKind = paramsRef.storageKind;
1578
+ const storageKey = paramsRef.storageKey;
1579
+ const expectedBytes = paramsRef.bytes;
1580
+ const expectedHash = paramsRef.hash;
1581
+ const expiresAt = paramsRef.expiresAt;
1415
1582
  if (
1416
1583
  storageKind !== 'r2' ||
1417
1584
  typeof storageKey !== 'string' ||
@@ -1419,10 +1586,37 @@ async function hydrateWorkflowRetryParams(input: {
1419
1586
  typeof expectedBytes !== 'number' ||
1420
1587
  !Number.isFinite(expectedBytes) ||
1421
1588
  typeof expectedHash !== 'string' ||
1422
- !expectedHash
1589
+ !expectedHash ||
1590
+ typeof expiresAt !== 'number' ||
1591
+ !Number.isFinite(expiresAt)
1423
1592
  ) {
1424
1593
  throw new Error('Invalid workflow retry params reference.');
1425
1594
  }
1595
+ return {
1596
+ storageKind,
1597
+ storageKey,
1598
+ bytes: expectedBytes,
1599
+ hash: expectedHash,
1600
+ expiresAt,
1601
+ };
1602
+ }
1603
+
1604
+ async function hydrateWorkflowRetryParams(input: {
1605
+ env: CoordinatorEnv;
1606
+ params: unknown;
1607
+ paramsRef: unknown;
1608
+ }): Promise<PlayWorkflowParams | null> {
1609
+ if (isRecord(input.params)) {
1610
+ return input.params as PlayWorkflowParams;
1611
+ }
1612
+ const paramsRef = parseWorkflowRetryParamsRef(input.paramsRef);
1613
+ if (!paramsRef) return null;
1614
+ const storageKey = paramsRef.storageKey;
1615
+ const expectedBytes = paramsRef.bytes;
1616
+ const expectedHash = paramsRef.hash;
1617
+ if (paramsRef.expiresAt <= Date.now()) {
1618
+ throw new Error('Workflow retry params reference expired.');
1619
+ }
1426
1620
  const object = await input.env.PLAYS_BUCKET.get(storageKey);
1427
1621
  if (!object) {
1428
1622
  throw new Error(`Workflow retry params missing from R2: ${storageKey}`);
@@ -1470,6 +1664,7 @@ async function claimWorkflowPlatformRetry(input: {
1470
1664
  claimed: boolean;
1471
1665
  attempts: number;
1472
1666
  params: PlayWorkflowParams | null;
1667
+ paramsRef?: WorkflowRetryParamsRef;
1473
1668
  }> {
1474
1669
  const body = await callRunScopedControl<{
1475
1670
  claimed?: unknown;
@@ -1488,10 +1683,12 @@ async function claimWorkflowPlatformRetry(input: {
1488
1683
  params: body.params,
1489
1684
  paramsRef: body.paramsRef,
1490
1685
  });
1686
+ const paramsRef = parseWorkflowRetryParamsRef(body.paramsRef) ?? undefined;
1491
1687
  return {
1492
1688
  claimed: body.claimed === true,
1493
1689
  attempts: typeof body.attempts === 'number' ? body.attempts : 0,
1494
1690
  params,
1691
+ paramsRef,
1495
1692
  };
1496
1693
  }
1497
1694
 
@@ -1502,6 +1699,12 @@ function workflowRetryInstanceId(runId: string, attempt: number): string {
1502
1699
  return `${base.slice(0, maxBaseLength)}-${suffix}`;
1503
1700
  }
1504
1701
 
1702
+ function normalizeWorkflowRunAttempt(value: unknown): number {
1703
+ return typeof value === 'number' && Number.isFinite(value)
1704
+ ? Math.max(0, Math.floor(value))
1705
+ : 0;
1706
+ }
1707
+
1505
1708
  async function restartWorkflowAfterPlatformReset(input: {
1506
1709
  env: CoordinatorEnv;
1507
1710
  ctx?: ExecutionContext;
@@ -1537,11 +1740,16 @@ async function restartWorkflowAfterPlatformReset(input: {
1537
1740
  const retryInstanceId = workflowRetryInstanceId(input.runId, claim.attempts);
1538
1741
  const retryStartedAt = Date.now();
1539
1742
  let retryInstance: WorkflowInstance | null = null;
1743
+ const retryParams = { ...claim.params, runAttempt: claim.attempts };
1540
1744
  try {
1541
1745
  retryInstance = await createDynamicWorkflowInstance({
1542
1746
  env: input.env,
1543
1747
  id: retryInstanceId,
1544
- params: claim.params,
1748
+ params: workflowCreateParams(retryParams, {
1749
+ params: retryParams,
1750
+ paramsRef: claim.paramsRef,
1751
+ }),
1752
+ metadataParams: retryParams,
1545
1753
  });
1546
1754
  await recordWorkflowInstanceId({
1547
1755
  env: input.env,
@@ -1591,10 +1799,14 @@ async function mintChildWorkflowExecutorToken(input: {
1591
1799
  maxCreditsPerRun?: number | null;
1592
1800
  }): Promise<string> {
1593
1801
  const response = await input.env.HARNESS.runtimeApiCall({
1802
+ contract: PLAY_RUNTIME_CONTRACT,
1594
1803
  executorToken: input.parentExecutorToken,
1595
1804
  baseUrl: input.baseUrl,
1596
- path: '/api/v2/plays/internal/child-executor-token',
1597
- headers: { 'x-deepline-request-id': crypto.randomUUID() },
1805
+ path: PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
1806
+ headers: {
1807
+ 'x-deepline-request-id': crypto.randomUUID(),
1808
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
1809
+ },
1598
1810
  timeoutMs: 15_000,
1599
1811
  body: {
1600
1812
  parentRunId: input.parentRunId,
@@ -1703,10 +1915,14 @@ async function createChildRuntimeDbSession(input: {
1703
1915
  }): Promise<CreateDbSessionResponse> {
1704
1916
  const decryptionKey = await generateDbSessionPostgresUrlDecryptionKey();
1705
1917
  const response = await input.env.HARNESS.runtimeApiCall({
1918
+ contract: PLAY_RUNTIME_CONTRACT,
1706
1919
  executorToken: input.childExecutorToken,
1707
1920
  baseUrl: input.baseUrl,
1708
- path: '/api/v2/plays/internal/runtime',
1709
- headers: { 'x-deepline-request-id': crypto.randomUUID() },
1921
+ path: PLAY_RUNTIME_API_COMPAT_PATH,
1922
+ headers: {
1923
+ 'x-deepline-request-id': crypto.randomUUID(),
1924
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
1925
+ },
1710
1926
  timeoutMs: 15_000,
1711
1927
  body: {
1712
1928
  action: 'create_db_session',
@@ -1812,10 +2028,14 @@ async function registerChildRunWithRuntime(input: {
1812
2028
  executionProfile: string;
1813
2029
  }): Promise<void> {
1814
2030
  const response = await input.env.HARNESS.runtimeApiCall({
2031
+ contract: PLAY_RUNTIME_CONTRACT,
1815
2032
  executorToken: input.childExecutorToken,
1816
2033
  baseUrl: input.baseUrl,
1817
- path: '/api/v2/plays/internal/runtime',
1818
- headers: { 'x-deepline-request-id': crypto.randomUUID() },
2034
+ path: PLAY_RUNTIME_API_COMPAT_PATH,
2035
+ headers: {
2036
+ 'x-deepline-request-id': crypto.randomUUID(),
2037
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
2038
+ },
1819
2039
  timeoutMs: 15_000,
1820
2040
  body: {
1821
2041
  action: 'start_inline_child_run',
@@ -1858,10 +2078,14 @@ async function markRegisteredChildRunFailed(input: {
1858
2078
  const message =
1859
2079
  input.error instanceof Error ? input.error.message : String(input.error);
1860
2080
  const response = await input.env.HARNESS.runtimeApiCall({
2081
+ contract: PLAY_RUNTIME_CONTRACT,
1861
2082
  executorToken: input.childExecutorToken,
1862
2083
  baseUrl: input.baseUrl,
1863
- path: '/api/v2/plays/internal/runtime',
1864
- headers: { 'x-deepline-request-id': crypto.randomUUID() },
2084
+ path: PLAY_RUNTIME_API_COMPAT_PATH,
2085
+ headers: {
2086
+ 'x-deepline-request-id': crypto.randomUUID(),
2087
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
2088
+ },
1865
2089
  timeoutMs: 15_000,
1866
2090
  body: {
1867
2091
  action: 'append_run_events',
@@ -1885,6 +2109,55 @@ async function markRegisteredChildRunFailed(input: {
1885
2109
  }
1886
2110
  }
1887
2111
 
2112
+ async function appendRegisteredChildRunTerminal(input: {
2113
+ env: CoordinatorEnv;
2114
+ baseUrl: string;
2115
+ childExecutorToken: string;
2116
+ childRunId: string;
2117
+ status: 'completed' | 'failed';
2118
+ result?: unknown;
2119
+ error?: string | null;
2120
+ }): Promise<void> {
2121
+ const event =
2122
+ input.status === 'completed'
2123
+ ? ({
2124
+ type: 'run.completed',
2125
+ runId: input.childRunId,
2126
+ source: 'worker',
2127
+ occurredAt: Date.now(),
2128
+ ...(input.result !== undefined ? { result: input.result } : {}),
2129
+ } satisfies PlayRunLedgerEvent)
2130
+ : ({
2131
+ type: 'run.failed',
2132
+ runId: input.childRunId,
2133
+ source: 'worker',
2134
+ occurredAt: Date.now(),
2135
+ error: input.error ?? 'Inline child Worker failed.',
2136
+ } satisfies PlayRunLedgerEvent);
2137
+ const response = await input.env.HARNESS.runtimeApiCall({
2138
+ contract: PLAY_RUNTIME_CONTRACT,
2139
+ executorToken: input.childExecutorToken,
2140
+ baseUrl: input.baseUrl,
2141
+ path: PLAY_RUNTIME_API_COMPAT_PATH,
2142
+ headers: {
2143
+ 'x-deepline-request-id': crypto.randomUUID(),
2144
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
2145
+ },
2146
+ timeoutMs: 15_000,
2147
+ body: {
2148
+ action: 'append_run_events',
2149
+ playId: input.childRunId,
2150
+ events: [event],
2151
+ },
2152
+ });
2153
+ if (response.status < 200 || response.status >= 300) {
2154
+ const text = response.body ?? '';
2155
+ throw new Error(
2156
+ `Inline child terminal append failed ${response.status}: ${text.slice(0, 800)}`,
2157
+ );
2158
+ }
2159
+ }
2160
+
1888
2161
  type CoordinatorRuntimeApiTiming = {
1889
2162
  phase: string;
1890
2163
  ms: number;
@@ -1924,12 +2197,14 @@ async function callRuntimeApiFromCoordinator(input: {
1924
2197
 
1925
2198
  const fetchStartedAt = Date.now();
1926
2199
  const response = await input.env.HARNESS.runtimeApiCall({
2200
+ contract: PLAY_RUNTIME_CONTRACT,
1927
2201
  executorToken: input.executorToken,
1928
2202
  baseUrl: input.baseUrl,
1929
- path: '/api/v2/plays/internal/runtime',
2203
+ path: PLAY_RUNTIME_API_COMPAT_PATH,
1930
2204
  body,
1931
2205
  headers: {
1932
2206
  'x-deepline-request-id': crypto.randomUUID(),
2207
+ [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
1933
2208
  },
1934
2209
  });
1935
2210
  recordTiming('coordinator.runtime_api.fetch', fetchStartedAt);
@@ -2022,19 +2297,38 @@ async function prepareInlineChildRunWithRuntime(input: {
2022
2297
  };
2023
2298
  }
2024
2299
 
2025
- function buildChildRunId(playName: string): string {
2026
- const slug =
2027
- playName
2028
- .toLowerCase()
2029
- .replace(/[^a-z0-9-]/g, '-')
2030
- .replace(/-+/g, '-')
2031
- .replace(/^-+|-+$/g, '')
2032
- .slice(0, 64) || 'play';
2033
- return `play/${slug}/run/${new Date()
2034
- .toISOString()
2035
- .replace(/[-:.]/g, '')
2036
- .replace('T', 't')
2037
- .replace('Z', '')}-${crypto.randomUUID().slice(0, 8)}`;
2300
+ function buildChildRunId(input: {
2301
+ playName: string;
2302
+ parentRunId: string;
2303
+ body: Record<string, unknown>;
2304
+ }): string {
2305
+ const { playName, parentRunId, body } = input;
2306
+ const governance = isRecord(body.internalRunPlay)
2307
+ ? body.internalRunPlay
2308
+ : null;
2309
+ // Adapt the coordinator's on-the-wire child-submit body to the shared
2310
+ // derivation. Output stays byte-identical to the previous inline
2311
+ // implementation (same fields → same digest → same run id), so child-submit
2312
+ // idempotency is preserved. See shared_libs/play-runtime/child-run-id.ts.
2313
+ return buildSharedChildRunId({
2314
+ childPlayName: playName,
2315
+ parentRunId,
2316
+ explicitIdempotencyKey:
2317
+ typeof body.childIdempotencyKey === 'string'
2318
+ ? body.childIdempotencyKey
2319
+ : null,
2320
+ parentPlayName:
2321
+ typeof governance?.parentPlayName === 'string'
2322
+ ? governance.parentPlayName
2323
+ : null,
2324
+ key: typeof governance?.key === 'string' ? governance.key : null,
2325
+ input: isRecord(body.input) ? body.input : {},
2326
+ graphHash: isRecord(body.manifest)
2327
+ ? typeof body.manifest.graphHash === 'string'
2328
+ ? body.manifest.graphHash
2329
+ : null
2330
+ : null,
2331
+ });
2038
2332
  }
2039
2333
 
2040
2334
  function normalizeRuntimeBaseUrl(value: unknown): string | null {
@@ -2087,8 +2381,7 @@ function validateChildSubmitBody(input: {
2087
2381
  const { parentRunId, body } = input;
2088
2382
  const manifest = body.manifest as PlayRuntimeManifest | undefined;
2089
2383
  const governance = body.internalRunPlay as
2090
- | PlayCallGovernanceSnapshot
2091
- | undefined;
2384
+ PlayCallGovernanceSnapshot | undefined;
2092
2385
  const childPlayName =
2093
2386
  typeof body.name === 'string' && body.name.trim()
2094
2387
  ? body.name.trim()
@@ -2219,6 +2512,7 @@ function buildChildWorkflowParams(input: {
2219
2512
  executorToken: childToken,
2220
2513
  baseUrl,
2221
2514
  integrationMode: normalizeIntegrationMode(body.integrationMode),
2515
+ force: body.force === true || body.forceToolRefresh === true,
2222
2516
  forceToolRefresh: body.forceToolRefresh === true,
2223
2517
  orgId,
2224
2518
  userEmail: typeof body.userEmail === 'string' ? body.userEmail : '',
@@ -2253,6 +2547,7 @@ function runRequestFromPlayWorkflowParams(
2253
2547
  graphHash: params.graphHash,
2254
2548
  userEmail: params.userEmail,
2255
2549
  runtimeInput: params.input,
2550
+ runAttempt: normalizeWorkflowRunAttempt(params.runAttempt),
2256
2551
  inlineCsv: params.inlineCsv ?? null,
2257
2552
  inputFiles:
2258
2553
  inputStorageKey && inputFileName
@@ -2280,8 +2575,11 @@ function runRequestFromPlayWorkflowParams(
2280
2575
  executionPlan: params.executionPlan ?? null,
2281
2576
  childPlayManifests: params.childPlayManifests ?? null,
2282
2577
  playCallGovernance: params.playCallGovernance ?? null,
2283
- force: params.forceToolRefresh === true,
2578
+ force: params.force === true || params.forceToolRefresh === true,
2579
+ forceToolRefresh: params.forceToolRefresh === true,
2284
2580
  preloadedDbSessions: params.preloadedDbSessions ?? null,
2581
+ runtimeTestFaultHeader: params.runtimeTestFaultHeader ?? null,
2582
+ testPolicyOverrides: params.testPolicyOverrides ?? null,
2285
2583
  inlineChildRunRegistered:
2286
2584
  params.runtimeBackend === 'cf_workflows_dynamic_worker_inline_child',
2287
2585
  coordinatorUrl: params.coordinatorUrl ?? null,
@@ -2376,7 +2674,11 @@ async function executeChildInline(input: {
2376
2674
  }
2377
2675
  const { manifest, governance, childPlayName, orgId, parentExecutorToken } =
2378
2676
  validated;
2379
- const childRunId = buildChildRunId(childPlayName);
2677
+ const childRunId = buildChildRunId({
2678
+ playName: childPlayName,
2679
+ parentRunId: input.parentRunId,
2680
+ body: input.body,
2681
+ });
2380
2682
  const timings: CoordinatorTiming[] = [];
2381
2683
  const trace = (
2382
2684
  phase: string,
@@ -2433,10 +2735,11 @@ async function executeChildInline(input: {
2433
2735
  input.body.parentPlayName.trim()
2434
2736
  ? input.body.parentPlayName.trim()
2435
2737
  : governance.parentPlayName;
2738
+ const baseUrl = resolveRuntimeBaseUrl(input.env, input.body);
2436
2739
  const { childToken, preloadedDbSessions, prepareTimings, transportTimings } =
2437
2740
  await prepareInlineChildRunWithRuntime({
2438
2741
  env: input.env,
2439
- baseUrl: resolveRuntimeBaseUrl(input.env, input.body),
2742
+ baseUrl,
2440
2743
  parentExecutorToken,
2441
2744
  parentRunId: input.parentRunId,
2442
2745
  parentPlayName,
@@ -2607,6 +2910,20 @@ async function executeChildInline(input: {
2607
2910
  error: error.message,
2608
2911
  },
2609
2912
  });
2913
+ const terminalAppendStartedAt = Date.now();
2914
+ await appendRegisteredChildRunTerminal({
2915
+ env: input.env,
2916
+ baseUrl,
2917
+ childExecutorToken: childToken,
2918
+ childRunId,
2919
+ status: 'failed',
2920
+ error: error.message,
2921
+ });
2922
+ trace(
2923
+ 'coordinator.inline_child_terminal_ledger_append',
2924
+ terminalAppendStartedAt,
2925
+ { status: 'failed' },
2926
+ );
2610
2927
  return {
2611
2928
  workflowId: childRunId,
2612
2929
  runId: childRunId,
@@ -2629,6 +2946,20 @@ async function executeChildInline(input: {
2629
2946
  mode: 'inline_dynamic_worker',
2630
2947
  },
2631
2948
  });
2949
+ const terminalAppendStartedAt = Date.now();
2950
+ await appendRegisteredChildRunTerminal({
2951
+ env: input.env,
2952
+ baseUrl,
2953
+ childExecutorToken: childToken,
2954
+ childRunId,
2955
+ status: 'completed',
2956
+ result: parsed.result,
2957
+ });
2958
+ trace(
2959
+ 'coordinator.inline_child_terminal_ledger_append',
2960
+ terminalAppendStartedAt,
2961
+ { status: 'completed' },
2962
+ );
2632
2963
  return {
2633
2964
  workflowId: childRunId,
2634
2965
  runId: childRunId,
@@ -2645,6 +2976,35 @@ async function executeChildInline(input: {
2645
2976
  }
2646
2977
  }
2647
2978
 
2979
+ function isRetryableChildWorkflowSubmitError(error: unknown): boolean {
2980
+ const message = error instanceof Error ? error.message : String(error);
2981
+ return /timed out|timeout|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|internal error/i.test(
2982
+ message,
2983
+ );
2984
+ }
2985
+
2986
+ function isRetryableChildWorkflowSubmitResponse(
2987
+ status: number,
2988
+ body: string,
2989
+ ): boolean {
2990
+ if (
2991
+ status === 408 ||
2992
+ status === 429 ||
2993
+ status === 502 ||
2994
+ status === 503 ||
2995
+ status === 504 ||
2996
+ status === 530
2997
+ ) {
2998
+ return true;
2999
+ }
3000
+ return (
3001
+ status === 500 &&
3002
+ /COORDINATOR_WORKFLOW_ROUTE_FAILED|timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|UND_ERR_CONNECT_TIMEOUT|internal error/i.test(
3003
+ body,
3004
+ )
3005
+ );
3006
+ }
3007
+
2648
3008
  async function submitChildWorkflowThroughCoordinator(input: {
2649
3009
  env: CoordinatorEnv;
2650
3010
  parentRunId: string;
@@ -2700,7 +3060,11 @@ async function submitChildWorkflowThroughCoordinator(input: {
2700
3060
  }
2701
3061
  const { manifest, governance, childPlayName, orgId, parentExecutorToken } =
2702
3062
  validated;
2703
- const childRunId = buildChildRunId(childPlayName);
3063
+ const childRunId = buildChildRunId({
3064
+ playName: childPlayName,
3065
+ parentRunId: input.parentRunId,
3066
+ body: input.body,
3067
+ });
2704
3068
  const baseUrl = resolveRuntimeBaseUrl(input.env, input.body);
2705
3069
 
2706
3070
  const tokenStartedAt = Date.now();
@@ -2783,38 +3147,96 @@ async function submitChildWorkflowThroughCoordinator(input: {
2783
3147
  );
2784
3148
 
2785
3149
  const workflowSubmitStartedAt = Date.now();
2786
- let response: Response;
2787
- try {
2788
- response = await handleWorkflowRoute({
2789
- runId: childRunId,
2790
- action: 'submit',
2791
- request: new Request(
2792
- `https://deepline.coordinator.internal/workflow/${encodeURIComponent(
2793
- childRunId,
2794
- )}/submit`,
3150
+ let response: Response | null = null;
3151
+ let responseText = '';
3152
+ let lastSubmitError: unknown = null;
3153
+ for (
3154
+ let attempt = 0;
3155
+ attempt <= CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS.length;
3156
+ attempt += 1
3157
+ ) {
3158
+ try {
3159
+ response = await handleWorkflowRoute({
3160
+ runId: childRunId,
3161
+ action: 'submit',
3162
+ request: new Request(
3163
+ `https://deepline.coordinator.internal/workflow/${encodeURIComponent(
3164
+ childRunId,
3165
+ )}/submit`,
3166
+ {
3167
+ method: 'POST',
3168
+ headers: { 'content-type': 'application/json' },
3169
+ body: JSON.stringify(params),
3170
+ },
3171
+ ),
3172
+ env: input.env,
3173
+ });
3174
+ } catch (error) {
3175
+ lastSubmitError = error;
3176
+ trace(
3177
+ 'coordinator.child_submit_workflow_attempt',
3178
+ workflowSubmitStartedAt,
3179
+ manifest.graphHash,
2795
3180
  {
2796
- method: 'POST',
2797
- headers: { 'content-type': 'application/json' },
2798
- body: JSON.stringify(params),
3181
+ childRunId,
3182
+ attempt: attempt + 1,
3183
+ status: 'threw',
3184
+ error: error instanceof Error ? error.message : String(error),
2799
3185
  },
2800
- ),
2801
- env: input.env,
2802
- });
2803
- } catch (error) {
2804
- await markRegisteredChildRunFailed({
2805
- env: input.env,
2806
- baseUrl,
2807
- childExecutorToken: childToken,
2808
- childRunId,
2809
- error,
2810
- }).catch((markError) => {
2811
- console.error('[coordinator] child workflow submit failure mark failed', {
3186
+ );
3187
+ if (
3188
+ attempt >= CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS.length ||
3189
+ !isRetryableChildWorkflowSubmitError(error)
3190
+ ) {
3191
+ await markRegisteredChildRunFailed({
3192
+ env: input.env,
3193
+ baseUrl,
3194
+ childExecutorToken: childToken,
3195
+ childRunId,
3196
+ error,
3197
+ }).catch((markError) => {
3198
+ console.error(
3199
+ '[coordinator] child workflow submit failure mark failed',
3200
+ {
3201
+ childRunId,
3202
+ error:
3203
+ markError instanceof Error
3204
+ ? markError.message
3205
+ : String(markError),
3206
+ },
3207
+ );
3208
+ });
3209
+ throw error;
3210
+ }
3211
+ await sleep(CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS[attempt] ?? 0);
3212
+ continue;
3213
+ }
3214
+
3215
+ responseText = await response.text().catch(() => '');
3216
+ trace(
3217
+ 'coordinator.child_submit_workflow_attempt',
3218
+ workflowSubmitStartedAt,
3219
+ manifest.graphHash,
3220
+ {
2812
3221
  childRunId,
2813
- error:
2814
- markError instanceof Error ? markError.message : String(markError),
2815
- });
2816
- });
2817
- throw error;
3222
+ attempt: attempt + 1,
3223
+ status: response.status,
3224
+ },
3225
+ );
3226
+ if (
3227
+ response.ok ||
3228
+ attempt >= CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS.length ||
3229
+ !isRetryableChildWorkflowSubmitResponse(response.status, responseText)
3230
+ ) {
3231
+ break;
3232
+ }
3233
+ lastSubmitError = `workflow submit returned ${response.status}: ${responseText.slice(0, 800)}`;
3234
+ await sleep(CHILD_WORKFLOW_SUBMIT_RETRY_DELAYS_MS[attempt] ?? 0);
3235
+ }
3236
+ if (!response) {
3237
+ throw lastSubmitError instanceof Error
3238
+ ? lastSubmitError
3239
+ : new Error(String(lastSubmitError));
2818
3240
  }
2819
3241
  trace(
2820
3242
  'coordinator.child_submit_workflow',
@@ -2822,7 +3244,6 @@ async function submitChildWorkflowThroughCoordinator(input: {
2822
3244
  manifest.graphHash,
2823
3245
  { childRunId, status: response.status },
2824
3246
  );
2825
- const responseText = await response.text().catch(() => '');
2826
3247
  if (!response.ok) {
2827
3248
  await markRegisteredChildRunFailed({
2828
3249
  env: input.env,
@@ -3156,6 +3577,11 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
3156
3577
  });
3157
3578
  }
3158
3579
  let dispatchedEvent = event;
3580
+ dispatchedEvent = await hydrateWorkflowLaunchParams({
3581
+ env: this.env,
3582
+ event: dispatchedEvent,
3583
+ trace,
3584
+ });
3159
3585
  try {
3160
3586
  dispatchedEvent = await hydrateWorkflowDbSessions({
3161
3587
  env: this.env,
@@ -3202,8 +3628,8 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
3202
3628
  await writeCoordinatorTerminalState(this.env, {
3203
3629
  runId: entryTrace.runId,
3204
3630
  status: 'failed',
3205
- error: normalizeCoordinatorWorkflowRuntimeFailure(hydrateError)
3206
- .message,
3631
+ error:
3632
+ normalizeCoordinatorWorkflowRuntimeFailure(hydrateError).message,
3207
3633
  }).catch(() => undefined);
3208
3634
  }
3209
3635
  }
@@ -3300,12 +3726,42 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
3300
3726
  }
3301
3727
  ).run(innerEvent, innerStep);
3302
3728
  const output = isRecord(result) ? result : null;
3729
+ const terminalResult = output?.result ?? result;
3303
3730
  const terminalStartedAt = Date.now();
3731
+ const childTerminalStartedAt = Date.now();
3732
+ try {
3733
+ const childTerminalPersisted =
3734
+ await appendWorkflowChildCompletedRunEvent({
3735
+ env,
3736
+ event: innerEvent,
3737
+ result: terminalResult,
3738
+ });
3739
+ if (childTerminalPersisted) {
3740
+ trace({
3741
+ runId: runIdForTrace,
3742
+ phase:
3743
+ 'coordinator.child_terminal_ledger_append_completed',
3744
+ ms: Date.now() - childTerminalStartedAt,
3745
+ graphHash,
3746
+ });
3747
+ }
3748
+ } catch (childTerminalError) {
3749
+ console.warn(
3750
+ '[coordinator] child terminal ledger append failed; preserving coordinator terminal cache',
3751
+ {
3752
+ runId: runIdForTrace,
3753
+ message:
3754
+ childTerminalError instanceof Error
3755
+ ? childTerminalError.message
3756
+ : String(childTerminalError),
3757
+ },
3758
+ );
3759
+ }
3304
3760
  try {
3305
3761
  await writeCoordinatorTerminalState(env, {
3306
3762
  runId: runIdForTrace,
3307
3763
  status: 'completed',
3308
- result: output?.result ?? result,
3764
+ result: terminalResult,
3309
3765
  totalRows: output?.totalRows ?? output?.outputRows ?? null,
3310
3766
  durationMs: output?.durationMs ?? null,
3311
3767
  playName:
@@ -3586,6 +4042,69 @@ async function coordinatorRouteFetch(
3586
4042
  });
3587
4043
  }
3588
4044
  }
4045
+ if (url.pathname === '/rate-acquire') {
4046
+ const authError = authorizeCoordinatorControlRequest({ request, env });
4047
+ if (authError) return authError;
4048
+ try {
4049
+ const body = (await request.json().catch(() => null)) as {
4050
+ bucketId?: unknown;
4051
+ rules?: unknown;
4052
+ requested?: unknown;
4053
+ } | null;
4054
+ const bucketId = typeof body?.bucketId === 'string' ? body.bucketId : '';
4055
+ if (!bucketId.trim()) {
4056
+ return Response.json(
4057
+ { error: 'bucketId is required.' },
4058
+ { status: 400 },
4059
+ );
4060
+ }
4061
+ const result = await callRateBucketControl<{
4062
+ granted: number;
4063
+ waitMs: number;
4064
+ }>(env, bucketId, '/rate-acquire', body);
4065
+ return Response.json(result);
4066
+ } catch (error) {
4067
+ return coordinatorRouteErrorResponse({
4068
+ logTag: '[coordinator.rate_acquire.error]',
4069
+ code: 'COORDINATOR_RATE_ROUTE_FAILED',
4070
+ phase: 'coordinator.rate.acquire',
4071
+ runId: null,
4072
+ error,
4073
+ });
4074
+ }
4075
+ }
4076
+ if (url.pathname === '/rate-penalize') {
4077
+ const authError = authorizeCoordinatorControlRequest({ request, env });
4078
+ if (authError) return authError;
4079
+ try {
4080
+ const body = (await request.json().catch(() => null)) as {
4081
+ bucketId?: unknown;
4082
+ cooldownMs?: unknown;
4083
+ } | null;
4084
+ const bucketId = typeof body?.bucketId === 'string' ? body.bucketId : '';
4085
+ if (!bucketId.trim()) {
4086
+ return Response.json(
4087
+ { error: 'bucketId is required.' },
4088
+ { status: 400 },
4089
+ );
4090
+ }
4091
+ await callRateBucketControl<{ ok?: unknown }>(
4092
+ env,
4093
+ bucketId,
4094
+ '/rate-penalize',
4095
+ body,
4096
+ );
4097
+ return Response.json({ ok: true });
4098
+ } catch (error) {
4099
+ return coordinatorRouteErrorResponse({
4100
+ logTag: '[coordinator.rate_penalize.error]',
4101
+ code: 'COORDINATOR_RATE_ROUTE_FAILED',
4102
+ phase: 'coordinator.rate.penalize',
4103
+ runId: null,
4104
+ error,
4105
+ });
4106
+ }
4107
+ }
3589
4108
  // Workflow routes: /workflow/{runId}/{action}
3590
4109
  const wfMatch = url.pathname.match(/^\/workflow\/([^/]+)(?:\/(.+))?$/);
3591
4110
  if (wfMatch) {
@@ -3676,7 +4195,7 @@ async function flushTailRunLogs(
3676
4195
  return;
3677
4196
  }
3678
4197
  await fetch(
3679
- `${env.DEEPLINE_API_BASE_URL}/api/v2/plays/internal/tail-log`,
4198
+ `${env.DEEPLINE_API_BASE_URL}${PLAY_RUNTIME_TAIL_LOG_COMPAT_PATH}`,
3680
4199
  {
3681
4200
  method: 'POST',
3682
4201
  headers: {
@@ -3779,7 +4298,10 @@ function coordinatorWorkflowRouteErrorResponse(input: {
3779
4298
  action: input.action,
3780
4299
  error: message,
3781
4300
  });
3782
- if (input.action === 'submit' && isTenantStorageDegradationError(input.error)) {
4301
+ if (
4302
+ input.action === 'submit' &&
4303
+ isTenantStorageDegradationError(input.error)
4304
+ ) {
3783
4305
  // De-redact: a tenant-storage failure gets the loud, actionable
3784
4306
  // WORKSPACE_STORAGE_NOT_READY message + repair instruction instead of the
3785
4307
  // generic "internal error". Same contract as the runtime route.
@@ -3996,12 +4518,15 @@ async function handleWorkflowRoute(input: {
3996
4518
  // logs. Log the tenant-storage classification loudly so the real
3997
4519
  // cause (and the WORKSPACE_STORAGE_NOT_READY repair path) is visible.
3998
4520
  if (storageNotReady) {
3999
- console.error('[coordinator.harness_prewarm_postgres.storage_not_ready]', {
4000
- runId: submittedRunId,
4001
- playName: params.playName,
4002
- code: WORKSPACE_STORAGE_NOT_READY_CODE,
4003
- error: error instanceof Error ? error.message : String(error),
4004
- });
4521
+ console.error(
4522
+ '[coordinator.harness_prewarm_postgres.storage_not_ready]',
4523
+ {
4524
+ runId: submittedRunId,
4525
+ playName: params.playName,
4526
+ code: WORKSPACE_STORAGE_NOT_READY_CODE,
4527
+ error: error instanceof Error ? error.message : String(error),
4528
+ },
4529
+ );
4005
4530
  }
4006
4531
  });
4007
4532
  input.ctx?.waitUntil(prewarmPromise);
@@ -4010,9 +4535,12 @@ async function handleWorkflowRoute(input: {
4010
4535
  params,
4011
4536
  });
4012
4537
  const workflowParams = dbSessionExternalization.params;
4538
+ workflowParams.submittedAt = Date.now();
4539
+ let launchState: Awaited<ReturnType<typeof persistWorkflowLaunchState>> =
4540
+ {};
4013
4541
  try {
4014
4542
  const retryStateStartedAt = Date.now();
4015
- const launchState = await persistWorkflowLaunchState({
4543
+ launchState = await persistWorkflowLaunchState({
4016
4544
  env,
4017
4545
  runId: submittedRunId,
4018
4546
  params: workflowParams,
@@ -4065,7 +4593,7 @@ async function handleWorkflowRoute(input: {
4065
4593
  error,
4066
4594
  });
4067
4595
  }
4068
- workflowParams.submittedAt = Date.now();
4596
+ const createParams = workflowCreateParams(workflowParams, launchState);
4069
4597
  let instance: WorkflowInstance | null = null;
4070
4598
  try {
4071
4599
  const submitClaimStartedAt = Date.now();
@@ -4167,7 +4695,8 @@ async function handleWorkflowRoute(input: {
4167
4695
  createDynamicWorkflowInstance({
4168
4696
  env,
4169
4697
  id: defaultInstanceId,
4170
- params: workflowParams,
4698
+ params: createParams,
4699
+ metadataParams: workflowParams,
4171
4700
  }),
4172
4701
  getExisting: () => env.PLAY_WORKFLOW.get(defaultInstanceId),
4173
4702
  onCreateFailure: async () => {
@@ -4737,7 +5266,8 @@ function stableHash(value: string): string {
4737
5266
  return (hash >>> 0).toString(36);
4738
5267
  }
4739
5268
 
4740
- const DYNAMIC_PLAY_WORKER_HARNESS_VERSION = 'h16-coordinator-only-prewarm';
5269
+ const DYNAMIC_PLAY_WORKER_HARNESS_VERSION =
5270
+ DYNAMIC_PLAY_WORKER_ARTIFACT_VERSION;
4741
5271
  const DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES = 64;
4742
5272
  const dynamicWorkerBundledCodeCache = new Map<string, string>();
4743
5273
 
@@ -5057,7 +5587,7 @@ export class TenantWorkflow extends WorkflowEntrypoint {
5057
5587
  const runId = payload && typeof payload.runId === "string" ? payload.runId : "warmup";
5058
5588
  const startedAt = Date.now();
5059
5589
  if (this.env.RUNTIME_API) {
5060
- await this.env.RUNTIME_API.fetch(new Request("https://deepline.runtime.internal/api/v2/plays/internal/runtime", {
5590
+ await this.env.RUNTIME_API.fetch(new Request("https://deepline.runtime.internal${PLAY_RUNTIME_API_COMPAT_PATH}", {
5061
5591
  method: "POST",
5062
5592
  headers: { "content-type": "application/json" },
5063
5593
  body: "{}"