deepline 0.1.78 → 0.1.79

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.
package/dist/cli/index.js CHANGED
@@ -229,10 +229,10 @@ var import_node_path2 = require("path");
229
229
 
230
230
  // src/release.ts
231
231
  var SDK_RELEASE = {
232
- version: "0.1.78",
232
+ version: "0.1.79",
233
233
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
234
234
  supportPolicy: {
235
- latest: "0.1.78",
235
+ latest: "0.1.79",
236
236
  minimumSupported: "0.1.53",
237
237
  deprecatedBelow: "0.1.53"
238
238
  }
@@ -7734,7 +7734,6 @@ ${hint}`;
7734
7734
  }
7735
7735
 
7736
7736
  // src/cli/commands/play.ts
7737
- var PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS = 2500;
7738
7737
  var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
7739
7738
  "--json",
7740
7739
  "--wait",
@@ -8639,10 +8638,6 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
8639
8638
  let eventCount = 0;
8640
8639
  let firstRunIdMs = null;
8641
8640
  let lastPhase = null;
8642
- const startRequest = {
8643
- ...input2.request,
8644
- waitForCompletionMs: typeof input2.request.waitForCompletionMs === "number" ? input2.request.waitForCompletionMs : PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS
8645
- };
8646
8641
  const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
8647
8642
  () => {
8648
8643
  timedOut = true;
@@ -8651,7 +8646,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
8651
8646
  Math.max(1, input2.waitTimeoutMs)
8652
8647
  );
8653
8648
  try {
8654
- for await (const event of input2.client.startPlayRunStream(startRequest, {
8649
+ for await (const event of input2.client.startPlayRunStream(input2.request, {
8655
8650
  signal: controller.signal
8656
8651
  })) {
8657
8652
  eventCount += 1;
@@ -206,10 +206,10 @@ import { join as join2 } from "path";
206
206
 
207
207
  // src/release.ts
208
208
  var SDK_RELEASE = {
209
- version: "0.1.78",
209
+ version: "0.1.79",
210
210
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
211
211
  supportPolicy: {
212
- latest: "0.1.78",
212
+ latest: "0.1.79",
213
213
  minimumSupported: "0.1.53",
214
214
  deprecatedBelow: "0.1.53"
215
215
  }
@@ -7737,7 +7737,6 @@ ${hint}`;
7737
7737
  }
7738
7738
 
7739
7739
  // src/cli/commands/play.ts
7740
- var PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS = 2500;
7741
7740
  var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
7742
7741
  "--json",
7743
7742
  "--wait",
@@ -8642,10 +8641,6 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
8642
8641
  let eventCount = 0;
8643
8642
  let firstRunIdMs = null;
8644
8643
  let lastPhase = null;
8645
- const startRequest = {
8646
- ...input2.request,
8647
- waitForCompletionMs: typeof input2.request.waitForCompletionMs === "number" ? input2.request.waitForCompletionMs : PLAY_START_STREAM_FAST_COMPLETION_WAIT_MS
8648
- };
8649
8644
  const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
8650
8645
  () => {
8651
8646
  timedOut = true;
@@ -8654,7 +8649,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
8654
8649
  Math.max(1, input2.waitTimeoutMs)
8655
8650
  );
8656
8651
  try {
8657
- for await (const event of input2.client.startPlayRunStream(startRequest, {
8652
+ for await (const event of input2.client.startPlayRunStream(input2.request, {
8658
8653
  signal: controller.signal
8659
8654
  })) {
8660
8655
  eventCount += 1;
package/dist/index.d.mts CHANGED
@@ -16,6 +16,7 @@ type PlayArtifactKind = (typeof PLAY_ARTIFACT_KINDS)[keyof typeof PLAY_ARTIFACT_
16
16
  interface PlayStaticPipelineSnapshot {
17
17
  tableNamespace?: string;
18
18
  inputFields?: string[];
19
+ rowKeyFields?: string[];
19
20
  csvArg?: string;
20
21
  hasInlineData?: boolean;
21
22
  csvDescription?: string;
@@ -39,11 +40,30 @@ interface PlaySheetColumnContractSnapshot {
39
40
  outputSqlName?: string;
40
41
  stepId?: string;
41
42
  toolId?: string;
43
+ isRowKey?: boolean;
42
44
  }
43
45
  interface PlaySheetContractSnapshot {
44
46
  tableNamespace: string;
45
47
  columns: PlaySheetColumnContractSnapshot[];
46
48
  }
49
+ type PlayStaticColumnProducerKindSnapshot = 'tool' | 'waterfall' | 'stepProgram' | 'playCall' | 'transform';
50
+ interface PlayStaticColumnProducerSnapshot {
51
+ id: string;
52
+ kind: PlayStaticColumnProducerKindSnapshot;
53
+ field: string;
54
+ toolId?: string;
55
+ playId?: string;
56
+ conditional?: boolean;
57
+ sourceRange?: PlayStaticSourceRangeSnapshot;
58
+ steps?: PlayStaticColumnProducerSnapshot[];
59
+ substep: PlayStaticSubstepSnapshot;
60
+ }
61
+ interface PlayStaticDatasetColumnSnapshot {
62
+ id: string;
63
+ source: PlaySheetColumnSourceSnapshot;
64
+ sqlName?: string;
65
+ producers: PlayStaticColumnProducerSnapshot[];
66
+ }
47
67
  interface PlayStaticSourceRangeSnapshot {
48
68
  sourcePath?: string;
49
69
  startLine: number;
@@ -68,8 +88,11 @@ type PlayStaticSubstepSnapshot = PlayStaticSubstepMetadataSnapshot & ({
68
88
  name?: string;
69
89
  tableNamespace?: string;
70
90
  inputFields?: string[];
91
+ rowKeyFields?: string[];
71
92
  outputFields?: string[];
93
+ columns?: PlayStaticDatasetColumnSnapshot[];
72
94
  waterfallIds?: string[];
95
+ steps?: PlayStaticSubstepSnapshot[];
73
96
  sheetContract?: PlaySheetContractSnapshot | null;
74
97
  description?: string;
75
98
  sourceRange?: PlayStaticSourceRangeSnapshot;
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ type PlayArtifactKind = (typeof PLAY_ARTIFACT_KINDS)[keyof typeof PLAY_ARTIFACT_
16
16
  interface PlayStaticPipelineSnapshot {
17
17
  tableNamespace?: string;
18
18
  inputFields?: string[];
19
+ rowKeyFields?: string[];
19
20
  csvArg?: string;
20
21
  hasInlineData?: boolean;
21
22
  csvDescription?: string;
@@ -39,11 +40,30 @@ interface PlaySheetColumnContractSnapshot {
39
40
  outputSqlName?: string;
40
41
  stepId?: string;
41
42
  toolId?: string;
43
+ isRowKey?: boolean;
42
44
  }
43
45
  interface PlaySheetContractSnapshot {
44
46
  tableNamespace: string;
45
47
  columns: PlaySheetColumnContractSnapshot[];
46
48
  }
49
+ type PlayStaticColumnProducerKindSnapshot = 'tool' | 'waterfall' | 'stepProgram' | 'playCall' | 'transform';
50
+ interface PlayStaticColumnProducerSnapshot {
51
+ id: string;
52
+ kind: PlayStaticColumnProducerKindSnapshot;
53
+ field: string;
54
+ toolId?: string;
55
+ playId?: string;
56
+ conditional?: boolean;
57
+ sourceRange?: PlayStaticSourceRangeSnapshot;
58
+ steps?: PlayStaticColumnProducerSnapshot[];
59
+ substep: PlayStaticSubstepSnapshot;
60
+ }
61
+ interface PlayStaticDatasetColumnSnapshot {
62
+ id: string;
63
+ source: PlaySheetColumnSourceSnapshot;
64
+ sqlName?: string;
65
+ producers: PlayStaticColumnProducerSnapshot[];
66
+ }
47
67
  interface PlayStaticSourceRangeSnapshot {
48
68
  sourcePath?: string;
49
69
  startLine: number;
@@ -68,8 +88,11 @@ type PlayStaticSubstepSnapshot = PlayStaticSubstepMetadataSnapshot & ({
68
88
  name?: string;
69
89
  tableNamespace?: string;
70
90
  inputFields?: string[];
91
+ rowKeyFields?: string[];
71
92
  outputFields?: string[];
93
+ columns?: PlayStaticDatasetColumnSnapshot[];
72
94
  waterfallIds?: string[];
95
+ steps?: PlayStaticSubstepSnapshot[];
73
96
  sheetContract?: PlaySheetContractSnapshot | null;
74
97
  description?: string;
75
98
  sourceRange?: PlayStaticSourceRangeSnapshot;
package/dist/index.js CHANGED
@@ -241,10 +241,10 @@ var import_node_path2 = require("path");
241
241
 
242
242
  // src/release.ts
243
243
  var SDK_RELEASE = {
244
- version: "0.1.78",
244
+ version: "0.1.79",
245
245
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
246
246
  supportPolicy: {
247
- latest: "0.1.78",
247
+ latest: "0.1.79",
248
248
  minimumSupported: "0.1.53",
249
249
  deprecatedBelow: "0.1.53"
250
250
  }
package/dist/index.mjs CHANGED
@@ -179,10 +179,10 @@ import { join as join2 } from "path";
179
179
 
180
180
  // src/release.ts
181
181
  var SDK_RELEASE = {
182
- version: "0.1.78",
182
+ version: "0.1.79",
183
183
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
184
184
  supportPolicy: {
185
- latest: "0.1.78",
185
+ latest: "0.1.79",
186
186
  minimumSupported: "0.1.53",
187
187
  deprecatedBelow: "0.1.53"
188
188
  }
@@ -53,6 +53,14 @@ import {
53
53
  decideWorkflowPlatformRetry,
54
54
  PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
55
55
  } from './workflow-retry';
56
+ import {
57
+ WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES,
58
+ WORKFLOW_RETRY_PARAMS_MAX_BYTES,
59
+ buildWorkflowRetryParams,
60
+ jsonByteLength,
61
+ workflowRetryParamsStorageKey,
62
+ type WorkflowRetryParamsRef,
63
+ } from './workflow-retry-state';
56
64
  import { sanitizeLiveLogLines } from './runtime/live-progress';
57
65
 
58
66
  export { DynamicWorkflowBinding };
@@ -563,7 +571,7 @@ const WORKFLOW_POOL_PROTOCOL_VERSION =
563
571
  const WORKFLOW_POOL_DO_NAME = 'workflow-pool:v2';
564
572
  const WORKFLOW_POOL_START_EVENT_TYPE = 'play_start';
565
573
  const WORKFLOW_POOL_TTL_MS = 8 * 60 * 1000;
566
- const WORKFLOW_POOL_TARGET_SIZE = 0;
574
+ const WORKFLOW_POOL_TARGET_SIZE = 4;
567
575
  const WORKFLOW_POOL_READY_TIMEOUT_MS = 1_500;
568
576
  const WORKFLOW_POOL_READY_POLL_MS = 250;
569
577
  const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
@@ -571,6 +579,8 @@ const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
571
579
  const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
572
580
  const WORKFLOW_POOL_START_ACK_TIMEOUT_MS = 750;
573
581
  const WORKFLOW_POOL_START_ACK_POLL_MS = 25;
582
+ const WORKFLOW_POOL_MISS_CLAIM_RETRY_TIMEOUT_MS = 3_000;
583
+ const WORKFLOW_POOL_MISS_CLAIM_RETRY_POLL_MS = 50;
574
584
  const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
575
585
  const SUBMIT_INITIAL_STATE_POLL_MS = 50;
576
586
  const WORKFLOW_RETRY_STATE_TTL_MS = 60 * 60 * 1000;
@@ -1117,6 +1127,115 @@ async function leaseWorkflowPoolId(
1117
1127
  return typeof body.id === 'string' && body.id ? body.id : null;
1118
1128
  }
1119
1129
 
1130
+ async function leaseWorkflowPoolIdWithMissRecovery(input: {
1131
+ env: CoordinatorEnv;
1132
+ runId: string;
1133
+ recordSubmitTiming: (timing: CoordinatorTiming) => void;
1134
+ graphHash?: string | null;
1135
+ }): Promise<{
1136
+ pooledInstanceId: string | null;
1137
+ missCounts: WorkflowPoolCounts | null;
1138
+ leaseError: string | null;
1139
+ }> {
1140
+ let leaseError: string | null = null;
1141
+ let pooledInstanceId = await leaseWorkflowPoolId(
1142
+ input.env,
1143
+ input.runId,
1144
+ ).catch((error) => {
1145
+ leaseError = error instanceof Error ? error.message : String(error);
1146
+ return null;
1147
+ });
1148
+ let missCounts = pooledInstanceId
1149
+ ? null
1150
+ : await workflowPoolCount(input.env).catch(() => null);
1151
+ if (
1152
+ pooledInstanceId ||
1153
+ leaseError ||
1154
+ !missCounts ||
1155
+ missCounts.available + missCounts.warming <= 0
1156
+ ) {
1157
+ return { pooledInstanceId, missCounts, leaseError };
1158
+ }
1159
+
1160
+ const recoveryStartedAt = Date.now();
1161
+ const refill = await refillWorkflowPool(input.env, {
1162
+ minAvailable: 1,
1163
+ waitReady: true,
1164
+ waitTimeoutMs: WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS,
1165
+ }).catch((error) => {
1166
+ input.recordSubmitTiming({
1167
+ phase: 'coordinator.workflow_pool_refill_on_miss',
1168
+ ms: Date.now() - recoveryStartedAt,
1169
+ graphHash: input.graphHash ?? null,
1170
+ extra: {
1171
+ status: 'failed',
1172
+ error: error instanceof Error ? error.message : String(error),
1173
+ available: missCounts?.available ?? null,
1174
+ warming: missCounts?.warming ?? null,
1175
+ },
1176
+ });
1177
+ return null;
1178
+ });
1179
+ if (refill) {
1180
+ input.recordSubmitTiming({
1181
+ phase: 'coordinator.workflow_pool_refill_on_miss',
1182
+ ms: Date.now() - recoveryStartedAt,
1183
+ graphHash: input.graphHash ?? null,
1184
+ extra: {
1185
+ status: 'ok',
1186
+ available: refill.available,
1187
+ warming: refill.warming,
1188
+ created: refill.created,
1189
+ promoted: refill.promoted,
1190
+ removed: refill.removed,
1191
+ waitedMs: refill.waitedMs,
1192
+ waitIterations: refill.waitIterations,
1193
+ },
1194
+ });
1195
+ }
1196
+
1197
+ let retryCount = 0;
1198
+ const retryStartedAt = Date.now();
1199
+ while (
1200
+ Date.now() - retryStartedAt <
1201
+ WORKFLOW_POOL_MISS_CLAIM_RETRY_TIMEOUT_MS
1202
+ ) {
1203
+ retryCount += 1;
1204
+ pooledInstanceId = await leaseWorkflowPoolId(input.env, input.runId).catch(
1205
+ (error) => {
1206
+ leaseError = error instanceof Error ? error.message : String(error);
1207
+ return null;
1208
+ },
1209
+ );
1210
+ if (pooledInstanceId || leaseError) {
1211
+ break;
1212
+ }
1213
+ missCounts = await workflowPoolCount(input.env).catch(() => missCounts);
1214
+ if (!missCounts || missCounts.available + missCounts.warming <= 0) {
1215
+ break;
1216
+ }
1217
+ await sleep(WORKFLOW_POOL_MISS_CLAIM_RETRY_POLL_MS);
1218
+ }
1219
+ input.recordSubmitTiming({
1220
+ phase: 'coordinator.workflow_pool_claim_retry',
1221
+ ms: Date.now() - retryStartedAt,
1222
+ graphHash: input.graphHash ?? null,
1223
+ extra: {
1224
+ pooled: Boolean(pooledInstanceId),
1225
+ retries: retryCount,
1226
+ ...(leaseError ? { error: leaseError } : {}),
1227
+ ...(missCounts
1228
+ ? {
1229
+ availableAfterRetry: missCounts.available,
1230
+ warmingAfterRetry: missCounts.warming,
1231
+ }
1232
+ : {}),
1233
+ },
1234
+ });
1235
+
1236
+ return { pooledInstanceId, missCounts, leaseError };
1237
+ }
1238
+
1120
1239
  async function mapRunToWorkflowInstance(input: {
1121
1240
  env: CoordinatorEnv;
1122
1241
  runId: string;
@@ -1191,56 +1310,120 @@ async function persistWorkflowRetryState(input: {
1191
1310
  runId: string;
1192
1311
  params: PlayWorkflowParams;
1193
1312
  }): Promise<void> {
1194
- const retryParams: PlayWorkflowParams = {
1195
- ...input.params,
1196
- dynamicWorkerCode: null,
1197
- contractSnapshot: stripRetrySourceSnapshot(input.params.contractSnapshot),
1198
- childPlayManifests: stripRetryChildManifestCode(
1199
- input.params.childPlayManifests,
1200
- ),
1201
- packagedFiles:
1202
- input.params.packagedFiles?.map((file) => ({
1203
- playPath: file.playPath,
1204
- storageKey: file.storageKey,
1205
- contentType: file.contentType,
1206
- bytes: file.bytes,
1207
- })) ?? null,
1313
+ const retryParams = buildWorkflowRetryParams(input.params);
1314
+ const paramsBytes = jsonByteLength(retryParams);
1315
+ if (paramsBytes > WORKFLOW_RETRY_PARAMS_MAX_BYTES) {
1316
+ throw new Error(
1317
+ `workflow retry params too large: ${paramsBytes} bytes exceeds ${WORKFLOW_RETRY_PARAMS_MAX_BYTES}. Pass large payloads as staged files or ctx.csv inputs instead of inline JSON.`,
1318
+ );
1319
+ }
1320
+ let body: {
1321
+ runId: string;
1322
+ params?: PlayWorkflowParams;
1323
+ paramsRef?: WorkflowRetryParamsRef;
1324
+ paramsBytes: number;
1325
+ ttlMs: number;
1208
1326
  };
1209
- await callWorkflowPool<{ ok?: unknown }>(input.env, '/run-retry-state-put', {
1210
- method: 'POST',
1211
- body: JSON.stringify({
1327
+ if (paramsBytes > WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES) {
1328
+ const serialized = JSON.stringify(retryParams);
1329
+ const hash = stableHash(serialized);
1330
+ const storageKey = workflowRetryParamsStorageKey({
1212
1331
  runId: input.runId,
1213
- params: retryParams,
1332
+ hash,
1333
+ });
1334
+ await input.env.PLAYS_BUCKET.put(storageKey, serialized, {
1335
+ httpMetadata: { contentType: 'application/json' },
1336
+ });
1337
+ body = {
1338
+ runId: input.runId,
1339
+ paramsRef: {
1340
+ storageKind: 'r2',
1341
+ storageKey,
1342
+ bytes: paramsBytes,
1343
+ hash,
1344
+ expiresAt: Date.now() + WORKFLOW_RETRY_STATE_TTL_MS,
1345
+ },
1346
+ paramsBytes,
1214
1347
  ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
1215
- }),
1216
- }).catch((error) => {
1217
- console.warn('[coordinator] workflow retry state persistence skipped', {
1348
+ };
1349
+ } else {
1350
+ body = {
1218
1351
  runId: input.runId,
1219
- error: error instanceof Error ? error.message : String(error),
1220
- });
1352
+ params: retryParams,
1353
+ paramsBytes,
1354
+ ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
1355
+ };
1356
+ }
1357
+ await callWorkflowPool<{ ok?: unknown }>(input.env, '/run-retry-state-put', {
1358
+ method: 'POST',
1359
+ body: JSON.stringify(body),
1221
1360
  });
1222
1361
  }
1223
1362
 
1224
- function stripRetrySourceSnapshot(snapshot: unknown): unknown {
1225
- if (!isRecord(snapshot)) return snapshot;
1226
- const rest = { ...snapshot };
1227
- delete rest.sourceCode;
1228
- delete rest.sourceFiles;
1229
- return rest;
1363
+ async function hydrateWorkflowRetryParams(input: {
1364
+ env: CoordinatorEnv;
1365
+ params: unknown;
1366
+ paramsRef: unknown;
1367
+ }): Promise<PlayWorkflowParams | null> {
1368
+ if (isRecord(input.params)) {
1369
+ return input.params as PlayWorkflowParams;
1370
+ }
1371
+ if (!isRecord(input.paramsRef)) {
1372
+ return null;
1373
+ }
1374
+ const storageKind = input.paramsRef.storageKind;
1375
+ const storageKey = input.paramsRef.storageKey;
1376
+ const expectedBytes = input.paramsRef.bytes;
1377
+ const expectedHash = input.paramsRef.hash;
1378
+ if (
1379
+ storageKind !== 'r2' ||
1380
+ typeof storageKey !== 'string' ||
1381
+ !storageKey.startsWith('plays/workflow-retry-params/') ||
1382
+ typeof expectedBytes !== 'number' ||
1383
+ !Number.isFinite(expectedBytes) ||
1384
+ typeof expectedHash !== 'string' ||
1385
+ !expectedHash
1386
+ ) {
1387
+ throw new Error('Invalid workflow retry params reference.');
1388
+ }
1389
+ const object = await input.env.PLAYS_BUCKET.get(storageKey);
1390
+ if (!object) {
1391
+ throw new Error(`Workflow retry params missing from R2: ${storageKey}`);
1392
+ }
1393
+ const text = await object.text();
1394
+ const actualBytes = new TextEncoder().encode(text).length;
1395
+ if (actualBytes !== expectedBytes) {
1396
+ throw new Error(
1397
+ `Workflow retry params byte length mismatch: expected ${expectedBytes}, got ${actualBytes}.`,
1398
+ );
1399
+ }
1400
+ const actualHash = stableHash(text);
1401
+ if (actualHash !== expectedHash) {
1402
+ throw new Error('Workflow retry params hash mismatch.');
1403
+ }
1404
+ const parsed = JSON.parse(text) as unknown;
1405
+ return isRecord(parsed) ? (parsed as PlayWorkflowParams) : null;
1230
1406
  }
1231
1407
 
1232
- function stripRetryChildManifestCode(
1233
- manifests: PlayRuntimeManifestMap | null | undefined,
1234
- ): PlayRuntimeManifestMap | null {
1235
- if (!manifests) return null;
1236
- const stripped: PlayRuntimeManifestMap = {};
1237
- for (const [key, manifest] of Object.entries(manifests)) {
1238
- const rest = { ...manifest };
1239
- delete rest.bundledCode;
1240
- delete rest.sourceCode;
1241
- stripped[key] = rest;
1242
- }
1243
- return stripped;
1408
+ function workflowRetryStatePersistenceErrorResponse(input: {
1409
+ runId: string;
1410
+ error: unknown;
1411
+ }): Response {
1412
+ const message =
1413
+ input.error instanceof Error ? input.error.message : String(input.error);
1414
+ return Response.json(
1415
+ {
1416
+ error: {
1417
+ code: 'WORKFLOW_RETRY_STATE_PERSISTENCE_FAILED',
1418
+ message:
1419
+ 'Failed to persist workflow retry state before dispatching the play run.',
1420
+ phase: 'coordinator_retry_state_persistence',
1421
+ runId: input.runId,
1422
+ cause: message,
1423
+ },
1424
+ },
1425
+ { status: 503 },
1426
+ );
1244
1427
  }
1245
1428
 
1246
1429
  async function claimWorkflowPlatformRetry(input: {
@@ -1255,6 +1438,7 @@ async function claimWorkflowPlatformRetry(input: {
1255
1438
  claimed?: unknown;
1256
1439
  attempts?: unknown;
1257
1440
  params?: unknown;
1441
+ paramsRef?: unknown;
1258
1442
  }>(input.env, '/run-retry-claim', {
1259
1443
  method: 'POST',
1260
1444
  body: JSON.stringify({
@@ -1262,10 +1446,15 @@ async function claimWorkflowPlatformRetry(input: {
1262
1446
  maxAttempts: PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
1263
1447
  }),
1264
1448
  });
1449
+ const params = await hydrateWorkflowRetryParams({
1450
+ env: input.env,
1451
+ params: body.params,
1452
+ paramsRef: body.paramsRef,
1453
+ });
1265
1454
  return {
1266
1455
  claimed: body.claimed === true,
1267
1456
  attempts: typeof body.attempts === 'number' ? body.attempts : 0,
1268
- params: isRecord(body.params) ? (body.params as PlayWorkflowParams) : null,
1457
+ params,
1269
1458
  };
1270
1459
  }
1271
1460
 
@@ -1719,17 +1908,13 @@ async function submitViaPooledWorkflow(input: {
1719
1908
  return null;
1720
1909
  }
1721
1910
  const leaseStartedAt = Date.now();
1722
- let leaseError: string | null = null;
1723
- const pooledInstanceId = await leaseWorkflowPoolId(
1724
- input.env,
1725
- input.params.runId,
1726
- ).catch((error) => {
1727
- leaseError = error instanceof Error ? error.message : String(error);
1728
- return null;
1729
- });
1730
- const missCounts = pooledInstanceId
1731
- ? null
1732
- : await workflowPoolCount(input.env).catch(() => null);
1911
+ const { pooledInstanceId, missCounts, leaseError } =
1912
+ await leaseWorkflowPoolIdWithMissRecovery({
1913
+ env: input.env,
1914
+ runId: input.params.runId,
1915
+ recordSubmitTiming: input.recordSubmitTiming,
1916
+ graphHash: input.params.graphHash ?? null,
1917
+ });
1733
1918
  input.recordSubmitTiming({
1734
1919
  phase: 'coordinator.workflow_pool_lease',
1735
1920
  ms: Date.now() - leaseStartedAt,
@@ -1746,30 +1931,6 @@ async function submitViaPooledWorkflow(input: {
1746
1931
  },
1747
1932
  });
1748
1933
 
1749
- if (!pooledInstanceId) {
1750
- // A pool miss must not block the user path. Refilling is handled by the
1751
- // caller's waitUntil after submit, so fall through to cold create now.
1752
- const counts =
1753
- missCounts ?? (await workflowPoolCount(input.env).catch(() => null));
1754
- input.recordSubmitTiming({
1755
- phase: 'coordinator.workflow_pool_refill_on_miss',
1756
- ms: 0,
1757
- graphHash: input.params.graphHash ?? null,
1758
- extra: {
1759
- skipped: true,
1760
- reason: 'pool_miss_does_not_block_submit',
1761
- ...(counts
1762
- ? {
1763
- available: counts.available,
1764
- warming: counts.warming,
1765
- waitedMs: 0,
1766
- waitIterations: 0,
1767
- }
1768
- : {}),
1769
- },
1770
- });
1771
- }
1772
-
1773
1934
  if (!pooledInstanceId) {
1774
1935
  return null;
1775
1936
  }
@@ -3606,11 +3767,40 @@ async function handleWorkflowRoute(input: {
3606
3767
  params,
3607
3768
  recordSubmitTiming,
3608
3769
  });
3609
- await persistWorkflowRetryState({
3610
- env,
3611
- runId: submittedRunId,
3612
- params: workflowParams,
3613
- });
3770
+ try {
3771
+ const retryStateStartedAt = Date.now();
3772
+ await persistWorkflowRetryState({
3773
+ env,
3774
+ runId: submittedRunId,
3775
+ params: workflowParams,
3776
+ });
3777
+ recordSubmitTiming({
3778
+ phase: 'coordinator.retry_state_persistence',
3779
+ ms: Date.now() - retryStateStartedAt,
3780
+ graphHash: params.graphHash ?? null,
3781
+ });
3782
+ } catch (error) {
3783
+ const errorMessage =
3784
+ error instanceof Error ? error.message : String(error);
3785
+ console.error('[coordinator] workflow retry state persistence failed', {
3786
+ code: 'WORKFLOW_RETRY_STATE_PERSISTENCE_FAILED',
3787
+ runId: submittedRunId,
3788
+ error: errorMessage,
3789
+ });
3790
+ recordSubmitTiming({
3791
+ phase: 'coordinator.retry_state_persistence',
3792
+ ms: 0,
3793
+ graphHash: params.graphHash ?? null,
3794
+ extra: {
3795
+ status: 'failed',
3796
+ error: errorMessage,
3797
+ },
3798
+ });
3799
+ return workflowRetryStatePersistenceErrorResponse({
3800
+ runId: submittedRunId,
3801
+ error,
3802
+ });
3803
+ }
3614
3804
  let instance: WorkflowInstance | null = null;
3615
3805
  try {
3616
3806
  const statusEventStartedAt = Date.now();
@@ -113,6 +113,8 @@ type WorkflowRunMapping = {
113
113
  type WorkflowRunRetryState = {
114
114
  runId: string;
115
115
  params: unknown;
116
+ paramsRef?: unknown;
117
+ paramsBytes?: number;
116
118
  retryAttempts: number;
117
119
  updatedAt: number;
118
120
  expiresAt: number;
@@ -1059,8 +1061,10 @@ export class PlayDedup implements DurableObject {
1059
1061
  ttlMs?: unknown;
1060
1062
  } | null;
1061
1063
  const runId = typeof body?.runId === 'string' ? body.runId : '';
1062
- if (!runId || !body || !('params' in body)) {
1063
- return new Response('runId and params are required', { status: 400 });
1064
+ if (!runId || !body || (!('params' in body) && !('paramsRef' in body))) {
1065
+ return new Response('runId and params or paramsRef are required', {
1066
+ status: 400,
1067
+ });
1064
1068
  }
1065
1069
  const now = Date.now();
1066
1070
  const ttlMs =
@@ -1075,7 +1079,14 @@ export class PlayDedup implements DurableObject {
1075
1079
  const existing = await this.state.storage.get<WorkflowRunRetryState>(key);
1076
1080
  const retryState = {
1077
1081
  runId,
1078
- params: body.params,
1082
+ params: 'params' in body ? body.params : null,
1083
+ paramsRef: 'paramsRef' in body ? body.paramsRef : null,
1084
+ paramsBytes:
1085
+ typeof (body as { paramsBytes?: unknown }).paramsBytes ===
1086
+ 'number' &&
1087
+ Number.isFinite((body as { paramsBytes?: number }).paramsBytes)
1088
+ ? (body as { paramsBytes: number }).paramsBytes
1089
+ : undefined,
1079
1090
  retryAttempts:
1080
1091
  existing?.runId === runId &&
1081
1092
  typeof existing.retryAttempts === 'number'
@@ -1128,6 +1139,8 @@ export class PlayDedup implements DurableObject {
1128
1139
  claimed: false,
1129
1140
  attempts: existing.retryAttempts,
1130
1141
  params: existing.params,
1142
+ paramsRef: existing.paramsRef ?? null,
1143
+ paramsBytes: existing.paramsBytes ?? null,
1131
1144
  };
1132
1145
  return;
1133
1146
  }
@@ -1142,6 +1155,8 @@ export class PlayDedup implements DurableObject {
1142
1155
  claimed: true,
1143
1156
  attempts: nextAttempts,
1144
1157
  params: existing.params,
1158
+ paramsRef: existing.paramsRef ?? null,
1159
+ paramsBytes: existing.paramsBytes ?? null,
1145
1160
  };
1146
1161
  });
1147
1162
  return new Response(JSON.stringify(response), {
@@ -0,0 +1,203 @@
1
+ import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
2
+ import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
3
+ import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
4
+ import type {
5
+ PlayRuntimeManifest,
6
+ PlayRuntimeManifestMap,
7
+ } from '../../../shared_libs/plays/compiler-manifest';
8
+
9
+ export const WORKFLOW_RETRY_STATE_TARGET_BYTES = 100_000;
10
+ export const WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES =
11
+ WORKFLOW_RETRY_STATE_TARGET_BYTES;
12
+ export const WORKFLOW_RETRY_PARAMS_MAX_BYTES = 1024 * 1024;
13
+
14
+ export type WorkflowRetryParamsRef = {
15
+ storageKind: 'r2';
16
+ storageKey: string;
17
+ bytes: number;
18
+ hash: string;
19
+ expiresAt: number;
20
+ };
21
+
22
+ export type WorkflowRetryPlayParams = {
23
+ runId: string;
24
+ playId: string;
25
+ playName: string;
26
+ artifactStorageKey: string;
27
+ artifactHash: string;
28
+ graphHash: string;
29
+ input: Record<string, unknown>;
30
+ inputFile?: {
31
+ name?: string;
32
+ r2Key?: string;
33
+ storageKey?: string;
34
+ path?: string;
35
+ fileName?: string;
36
+ logicalPath?: string;
37
+ contentType?: string;
38
+ bytes?: number;
39
+ } | null;
40
+ inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
41
+ packagedFiles?: Array<{
42
+ playPath: string;
43
+ storageKey: string;
44
+ contentType?: string;
45
+ bytes?: number;
46
+ inlineText?: string;
47
+ }> | null;
48
+ contractSnapshot?: unknown;
49
+ executionPlan?: ExecutionPlan | null;
50
+ childPlayManifests?: PlayRuntimeManifestMap | null;
51
+ playCallGovernance?: PlayCallGovernanceSnapshot | null;
52
+ preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
53
+ preloadedDbSessionRef?: {
54
+ runId: string;
55
+ sessionCount: number;
56
+ expiresAt: number;
57
+ } | null;
58
+ dynamicWorkerCode?: string | null;
59
+ executorToken: string;
60
+ baseUrl: string;
61
+ orgId: string;
62
+ userEmail: string;
63
+ userId?: string | null;
64
+ runtimeBackend: string;
65
+ dedupBackend: string;
66
+ totalRows?: number;
67
+ coordinatorUrl?: string | null;
68
+ coordinatorInternalToken?: string | null;
69
+ };
70
+
71
+ export type WorkflowRetryStatePayload<TParams = WorkflowRetryPlayParams> =
72
+ | {
73
+ params: TParams;
74
+ paramsRef?: null;
75
+ paramsBytes: number;
76
+ }
77
+ | {
78
+ params: null;
79
+ paramsRef: WorkflowRetryParamsRef;
80
+ paramsBytes: number;
81
+ };
82
+
83
+ export function buildWorkflowRetryParams<
84
+ TParams extends WorkflowRetryPlayParams,
85
+ >(params: TParams): TParams {
86
+ const retryParams = {
87
+ ...params,
88
+ dynamicWorkerCode: null,
89
+ contractSnapshot: stripRetrySourceSnapshot(params.contractSnapshot),
90
+ childPlayManifests: stripRetryChildManifestCode(params.childPlayManifests),
91
+ packagedFiles: stripRetryPackagedFiles(params.packagedFiles),
92
+ } satisfies WorkflowRetryPlayParams as TParams;
93
+ if (jsonByteLength(retryParams) <= WORKFLOW_RETRY_STATE_TARGET_BYTES) {
94
+ return retryParams;
95
+ }
96
+ return {
97
+ ...retryParams,
98
+ contractSnapshot: stripRetryContractSnapshotToArtifact(
99
+ retryParams.contractSnapshot,
100
+ params,
101
+ ),
102
+ childPlayManifests: stripRetryChildManifestToArtifact(
103
+ retryParams.childPlayManifests,
104
+ ),
105
+ } satisfies WorkflowRetryPlayParams as TParams;
106
+ }
107
+
108
+ export function jsonByteLength(value: unknown): number {
109
+ return new TextEncoder().encode(JSON.stringify(value)).length;
110
+ }
111
+
112
+ export function workflowRetryParamsStorageKey(input: {
113
+ runId: string;
114
+ hash: string;
115
+ }): string {
116
+ const safeRunId = input.runId
117
+ .toLowerCase()
118
+ .replace(/[^a-z0-9_-]+/g, '-')
119
+ .slice(0, 140);
120
+ return `plays/workflow-retry-params/${safeRunId || 'run'}/${input.hash}.json`;
121
+ }
122
+
123
+ function stripRetryPackagedFiles(
124
+ files: WorkflowRetryPlayParams['packagedFiles'],
125
+ ): WorkflowRetryPlayParams['packagedFiles'] {
126
+ return (
127
+ files?.map((file) => ({
128
+ playPath: file.playPath,
129
+ storageKey: file.storageKey,
130
+ contentType: file.contentType,
131
+ bytes: file.bytes,
132
+ })) ?? null
133
+ );
134
+ }
135
+
136
+ function stripRetrySourceSnapshot(snapshot: unknown): unknown {
137
+ if (!isRecord(snapshot)) return snapshot;
138
+ const rest = { ...snapshot };
139
+ delete rest.sourceCode;
140
+ delete rest.sourceFiles;
141
+ delete rest.bundledCode;
142
+ return rest;
143
+ }
144
+
145
+ function stripRetryContractSnapshotToArtifact(
146
+ snapshot: unknown,
147
+ params: WorkflowRetryPlayParams,
148
+ ): unknown {
149
+ if (!isRecord(snapshot)) return snapshot;
150
+ return {
151
+ source: snapshot.source ?? 'artifact',
152
+ revisionVersion: snapshot.revisionVersion ?? null,
153
+ staticPipeline: snapshot.staticPipeline ?? null,
154
+ billingLimit: snapshot.billingLimit ?? null,
155
+ artifactMetadata: {
156
+ storageKey: params.artifactStorageKey,
157
+ artifactHash: params.artifactHash,
158
+ graphHash: params.graphHash,
159
+ },
160
+ codeFormat: snapshot.codeFormat ?? null,
161
+ compatibility: snapshot.compatibility ?? null,
162
+ };
163
+ }
164
+
165
+ function stripRetryChildManifestCode(
166
+ manifests: PlayRuntimeManifestMap | null | undefined,
167
+ ): PlayRuntimeManifestMap | null {
168
+ if (!manifests) return null;
169
+ const stripped: PlayRuntimeManifestMap = {};
170
+ for (const [key, manifest] of Object.entries(manifests)) {
171
+ const rest = { ...manifest };
172
+ delete rest.bundledCode;
173
+ delete rest.sourceCode;
174
+ delete (rest as Record<string, unknown>).sourceMap;
175
+ stripped[key] = rest;
176
+ }
177
+ return stripped;
178
+ }
179
+
180
+ function stripRetryChildManifestToArtifact(
181
+ manifests: PlayRuntimeManifestMap | null | undefined,
182
+ ): PlayRuntimeManifestMap | null {
183
+ if (!manifests) return null;
184
+ const stripped: PlayRuntimeManifestMap = {};
185
+ for (const [key, manifest] of Object.entries(manifests)) {
186
+ stripped[key] = {
187
+ playName: manifest.playName,
188
+ graphHash: manifest.graphHash,
189
+ artifactStorageKey: manifest.artifactStorageKey,
190
+ artifactHash: manifest.artifactHash,
191
+ staticPipelineHash: manifest.staticPipelineHash,
192
+ staticPipeline: manifest.staticPipeline,
193
+ compiledAt: manifest.compiledAt,
194
+ compilerVersion: manifest.compilerVersion,
195
+ maxCreditsPerRun: manifest.maxCreditsPerRun,
196
+ };
197
+ }
198
+ return stripped;
199
+ }
200
+
201
+ function isRecord(value: unknown): value is Record<string, unknown> {
202
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
203
+ }
@@ -50,10 +50,10 @@ export type SdkRelease = {
50
50
  };
51
51
 
52
52
  export const SDK_RELEASE = {
53
- version: '0.1.78',
53
+ version: '0.1.79',
54
54
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
55
55
  supportPolicy: {
56
- latest: '0.1.78',
56
+ latest: '0.1.79',
57
57
  minimumSupported: '0.1.53',
58
58
  deprecatedBelow: '0.1.53',
59
59
  },
@@ -3,6 +3,7 @@ import { normalizeTableNamespace } from './row-identity';
3
3
  export interface PlayStaticPipeline {
4
4
  tableNamespace?: string;
5
5
  inputFields?: string[];
6
+ rowKeyFields?: string[];
6
7
  csvArg?: string;
7
8
  hasInlineData?: boolean;
8
9
  csvDescription?: string;
@@ -32,6 +33,7 @@ export interface PlaySheetColumnContract {
32
33
  outputSqlName?: string;
33
34
  stepId?: string;
34
35
  toolId?: string;
36
+ isRowKey?: boolean;
35
37
  }
36
38
 
37
39
  export interface PlaySheetContract {
@@ -39,6 +41,40 @@ export interface PlaySheetContract {
39
41
  columns: PlaySheetColumnContract[];
40
42
  }
41
43
 
44
+ export type PlayStaticColumnProducerKind =
45
+ | 'tool'
46
+ | 'waterfall'
47
+ | 'stepProgram'
48
+ | 'playCall'
49
+ | 'transform';
50
+
51
+ export interface PlayStaticColumnProducer {
52
+ id: string;
53
+ kind: PlayStaticColumnProducerKind;
54
+ field: string;
55
+ toolId?: string;
56
+ playId?: string;
57
+ conditional?: boolean;
58
+ sourceRange?: PlayStaticSourceRange;
59
+ steps?: PlayStaticColumnProducer[];
60
+ substep: PlayStaticSubstep;
61
+ }
62
+
63
+ export interface PlayStaticDatasetColumn {
64
+ id: string;
65
+ source: PlaySheetColumnSource;
66
+ sqlName?: string;
67
+ producers: PlayStaticColumnProducer[];
68
+ }
69
+
70
+ export interface PlayCompiledStaticGraph {
71
+ topLevel: PlayStaticSubstep[];
72
+ datasets: Array<{
73
+ tableNamespace: string;
74
+ columns: PlayStaticDatasetColumn[];
75
+ }>;
76
+ }
77
+
42
78
  export function ensureCompiledSheetContract(
43
79
  pipeline: PlayStaticPipeline | null | undefined,
44
80
  ): PlayStaticPipeline | null | undefined {
@@ -92,8 +128,27 @@ function truncateStaticSubstepsForStorage(
92
128
  return {
93
129
  ...base,
94
130
  inputFields: base.inputFields ? [...base.inputFields] : undefined,
131
+ rowKeyFields: base.rowKeyFields ? [...base.rowKeyFields] : undefined,
95
132
  outputFields: base.outputFields ? [...base.outputFields] : undefined,
133
+ columns: base.columns
134
+ ? base.columns.map((column) => ({
135
+ ...column,
136
+ producers: column.producers.map((producer) => ({
137
+ ...producer,
138
+ sourceRange: cloneStorageSafeSourceRange(producer.sourceRange),
139
+ steps: producer.steps
140
+ ? producer.steps.map((stepProducer) => ({
141
+ ...stepProducer,
142
+ sourceRange: cloneStorageSafeSourceRange(
143
+ stepProducer.sourceRange,
144
+ ),
145
+ }))
146
+ : undefined,
147
+ })),
148
+ }))
149
+ : undefined,
96
150
  waterfallIds: base.waterfallIds ? [...base.waterfallIds] : undefined,
151
+ steps: truncateStaticSubstepsForStorage(base.steps, input),
97
152
  sheetContract: cloneStorageSafeSheetContract(base.sheetContract),
98
153
  };
99
154
  }
@@ -153,6 +208,9 @@ export function truncateStaticPipelineForStorage(
153
208
  return {
154
209
  ...pipeline,
155
210
  inputFields: pipeline.inputFields ? [...pipeline.inputFields] : undefined,
211
+ rowKeyFields: pipeline.rowKeyFields
212
+ ? [...pipeline.rowKeyFields]
213
+ : undefined,
156
214
  fields: [...(pipeline.fields ?? [])],
157
215
  stages: truncateStaticSubstepsForStorage(pipeline.stages, {
158
216
  embeddedPlayCallPipelineDepth,
@@ -198,8 +256,11 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
198
256
  name?: string;
199
257
  tableNamespace?: string;
200
258
  inputFields?: string[];
259
+ rowKeyFields?: string[];
201
260
  outputFields?: string[];
261
+ columns?: PlayStaticDatasetColumn[];
202
262
  waterfallIds?: string[];
263
+ steps?: PlayStaticSubstep[];
203
264
  sheetContract?: PlaySheetContract | null;
204
265
  description?: string;
205
266
  sourceRange?: PlayStaticSourceRange;
@@ -210,6 +271,8 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
210
271
  type: 'tool';
211
272
  toolId: string;
212
273
  field: string;
274
+ paramsSource?: string;
275
+ sourceText?: string;
213
276
  description?: string;
214
277
  inLoop?: boolean;
215
278
  isEventWait?: boolean;
@@ -263,6 +326,7 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
263
326
  | {
264
327
  type: 'run_javascript';
265
328
  alias: string;
329
+ sourceText?: string;
266
330
  description?: string;
267
331
  sourceRange?: PlayStaticSourceRange;
268
332
  callDepth?: number;
@@ -271,6 +335,7 @@ export type PlayStaticSubstep = PlayStaticSubstepMetadata &
271
335
  | {
272
336
  type: 'code';
273
337
  field: string;
338
+ sourceText?: string;
274
339
  description?: string;
275
340
  sourceRange?: PlayStaticSourceRange;
276
341
  callDepth?: number;
@@ -289,6 +354,12 @@ export function getCompiledPipelineSubsteps(
289
354
 
290
355
  export function getTopLevelPipelineSubsteps(
291
356
  pipeline: PlayStaticPipeline | null | undefined,
357
+ ): PlayStaticSubstep[] {
358
+ return compileStaticGraph(pipeline).topLevel;
359
+ }
360
+
361
+ function getRawTopLevelPipelineSubsteps(
362
+ pipeline: PlayStaticPipeline | null | undefined,
292
363
  ): PlayStaticSubstep[] {
293
364
  if (!pipeline) {
294
365
  return [];
@@ -328,6 +399,14 @@ export function flattenStaticSubsteps(
328
399
 
329
400
  for (const substep of substeps) {
330
401
  flattened.push(substep);
402
+ if (substep.type === 'dataset' && substep.steps?.length) {
403
+ flattened.push(...flattenStaticSubsteps(substep.steps));
404
+ continue;
405
+ }
406
+ if (substep.type === 'step_suite') {
407
+ flattened.push(...flattenStaticSubsteps(substep.steps));
408
+ continue;
409
+ }
331
410
  if (substep.type === 'play_call' && substep.pipeline) {
332
411
  const nestedSubsteps = getCompiledPipelineSubsteps(substep.pipeline);
333
412
  if (nestedSubsteps.length > 0) {
@@ -345,6 +424,137 @@ export function flattenStaticPipeline(
345
424
  return flattenStaticSubsteps(getCompiledPipelineSubsteps(pipeline));
346
425
  }
347
426
 
427
+ export function compileStaticGraph(
428
+ pipeline: PlayStaticPipeline | null | undefined,
429
+ ): PlayCompiledStaticGraph {
430
+ const rawTopLevel = getRawTopLevelPipelineSubsteps(pipeline);
431
+ const datasets: PlayCompiledStaticGraph['datasets'] = [];
432
+ const topLevel = rawTopLevel.map((substep) => {
433
+ if (substep.type !== 'dataset') {
434
+ return substep;
435
+ }
436
+ const columns = compileDatasetColumns(substep);
437
+ const tableNamespace = (substep.tableNamespace ?? substep.field).trim();
438
+ if (tableNamespace) {
439
+ datasets.push({ tableNamespace, columns });
440
+ }
441
+ return {
442
+ ...substep,
443
+ columns,
444
+ } satisfies PlayStaticSubstep;
445
+ });
446
+ return { topLevel, datasets };
447
+ }
448
+
449
+ function compileDatasetColumns(
450
+ dataset: Extract<PlayStaticSubstep, { type: 'dataset' }>,
451
+ ): PlayStaticDatasetColumn[] {
452
+ const columnsById = new Map<string, PlayStaticDatasetColumn>();
453
+ const ensureColumn = (
454
+ id: string,
455
+ source: PlaySheetColumnSource,
456
+ sqlName?: string,
457
+ ) => {
458
+ const trimmed = id.trim();
459
+ if (!trimmed) return null;
460
+ const existing = columnsById.get(trimmed);
461
+ if (existing) {
462
+ if (!existing.sqlName && sqlName) existing.sqlName = sqlName;
463
+ return existing;
464
+ }
465
+ const column: PlayStaticDatasetColumn = {
466
+ id: trimmed,
467
+ source,
468
+ ...(sqlName ? { sqlName } : {}),
469
+ producers: [],
470
+ };
471
+ columnsById.set(trimmed, column);
472
+ return column;
473
+ };
474
+
475
+ for (const column of dataset.sheetContract?.columns ?? []) {
476
+ ensureColumn(column.id, column.source, column.sqlName);
477
+ }
478
+ for (const field of dataset.inputFields ?? []) {
479
+ ensureColumn(field, 'input', sqlSafePlayColumnName(field));
480
+ }
481
+ for (const field of dataset.outputFields ?? []) {
482
+ ensureColumn(field, 'datasetColumn', sqlSafePlayColumnName(field));
483
+ }
484
+
485
+ for (const substep of dataset.steps ?? []) {
486
+ const field = fieldForColumnProducer(substep);
487
+ if (!field) continue;
488
+ const column = ensureColumn(
489
+ field,
490
+ 'datasetColumn',
491
+ sqlSafePlayColumnName(field),
492
+ );
493
+ if (!column) continue;
494
+ column.producers.push(columnProducerFromSubstep(substep, field));
495
+ }
496
+
497
+ return [...columnsById.values()];
498
+ }
499
+
500
+ function fieldForColumnProducer(substep: PlayStaticSubstep): string | null {
501
+ if ('field' in substep && typeof substep.field === 'string') {
502
+ return substep.field.trim() || null;
503
+ }
504
+ if (substep.type === 'run_javascript') {
505
+ return substep.alias.trim() || null;
506
+ }
507
+ return null;
508
+ }
509
+
510
+ function columnProducerFromSubstep(
511
+ substep: PlayStaticSubstep,
512
+ field: string,
513
+ ): PlayStaticColumnProducer {
514
+ const steps =
515
+ substep.type === 'step_suite'
516
+ ? substep.steps
517
+ .map((step) => {
518
+ const stepField = fieldForColumnProducer(step) ?? field;
519
+ return columnProducerFromSubstep(step, stepField);
520
+ })
521
+ .filter((producer) => producer.field.trim())
522
+ : undefined;
523
+ const kind: PlayStaticColumnProducerKind =
524
+ substep.type === 'tool'
525
+ ? 'tool'
526
+ : substep.type === 'waterfall'
527
+ ? 'waterfall'
528
+ : substep.type === 'step_suite'
529
+ ? 'stepProgram'
530
+ : substep.type === 'play_call'
531
+ ? 'playCall'
532
+ : 'transform';
533
+ return {
534
+ id: producerId(substep, field),
535
+ kind,
536
+ field,
537
+ ...(substep.type === 'tool' ? { toolId: substep.toolId } : {}),
538
+ ...(substep.type === 'play_call' ? { playId: substep.playId } : {}),
539
+ ...(substep.conditional ? { conditional: true } : {}),
540
+ ...(substep.sourceRange ? { sourceRange: substep.sourceRange } : {}),
541
+ ...(steps && steps.length > 0 ? { steps } : {}),
542
+ substep,
543
+ };
544
+ }
545
+
546
+ function producerId(substep: PlayStaticSubstep, field: string): string {
547
+ if (substep.type === 'tool') return `${field}:tool:${substep.toolId}`;
548
+ if (substep.type === 'waterfall') {
549
+ return `${field}:waterfall:${substep.id ?? substep.tool ?? 'unknown'}`;
550
+ }
551
+ if (substep.type === 'play_call') return `${field}:play:${substep.playId}`;
552
+ if (substep.type === 'step_suite') return `${field}:steps`;
553
+ if (substep.type === 'run_javascript')
554
+ return `${field}:transform:${substep.alias}`;
555
+ return `${field}:transform`;
556
+ }
557
+
348
558
  export function resolveSheetContractForTableNamespace(
349
559
  pipeline: PlayStaticPipeline | null | undefined,
350
560
  tableNamespace: string | null | undefined,
@@ -400,6 +610,50 @@ export function resolveSheetContractForTableNamespace(
400
610
  return resolveFromPipeline(pipeline);
401
611
  }
402
612
 
613
+ export function resolveStaticDatasetColumnsForTableNamespace(
614
+ pipeline: PlayStaticPipeline | null | undefined,
615
+ tableNamespace: string | null | undefined,
616
+ ): PlayStaticDatasetColumn[] {
617
+ const requestedNamespace = tableNamespace?.trim();
618
+ if (!pipeline || !requestedNamespace) {
619
+ return [];
620
+ }
621
+ const normalizedNamespace = normalizeTableNamespace(requestedNamespace);
622
+ const seen = new Set<PlayStaticPipeline>();
623
+
624
+ const resolveFromPipeline = (
625
+ currentPipeline: PlayStaticPipeline | null | undefined,
626
+ ): PlayStaticDatasetColumn[] | null => {
627
+ if (!currentPipeline || seen.has(currentPipeline)) {
628
+ return null;
629
+ }
630
+ seen.add(currentPipeline);
631
+
632
+ const compiled = compileStaticGraph(currentPipeline);
633
+ const matchingDataset = compiled.datasets.find(
634
+ (dataset) =>
635
+ normalizeTableNamespace(dataset.tableNamespace) ===
636
+ normalizedNamespace,
637
+ );
638
+ if (matchingDataset) {
639
+ return matchingDataset.columns;
640
+ }
641
+
642
+ for (const substep of getCompiledPipelineSubsteps(currentPipeline)) {
643
+ if (substep.type === 'play_call') {
644
+ const nestedColumns = resolveFromPipeline(substep.pipeline);
645
+ if (nestedColumns) {
646
+ return nestedColumns;
647
+ }
648
+ }
649
+ }
650
+
651
+ return null;
652
+ };
653
+
654
+ return resolveFromPipeline(pipeline) ?? [];
655
+ }
656
+
403
657
  export function sqlSafePlayColumnName(id: string): string {
404
658
  const normalized = id
405
659
  .trim()
@@ -441,8 +695,14 @@ export function compileSheetContract(pipeline: PlayStaticPipeline): {
441
695
  const inputFields = pipeline.inputFields?.length
442
696
  ? pipeline.inputFields
443
697
  : [tableNamespace];
698
+ const rowKeyFieldSet = new Set(pipeline.rowKeyFields ?? []);
444
699
  for (const inputField of inputFields) {
445
- addColumn({ id: inputField, source: 'input', field: inputField });
700
+ addColumn({
701
+ id: inputField,
702
+ source: 'input',
703
+ field: inputField,
704
+ ...(rowKeyFieldSet.has(inputField) ? { isRowKey: true } : {}),
705
+ });
446
706
  }
447
707
 
448
708
  for (const field of pipeline.fields) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {