deepline 0.1.26 → 0.1.27

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
@@ -266,7 +266,7 @@ function saveProjectDeeplineEnvValues(baseUrl, values, startDir = projectEnvStar
266
266
  }
267
267
 
268
268
  // src/version.ts
269
- var SDK_VERSION = "0.1.26";
269
+ var SDK_VERSION = "0.1.27";
270
270
  var SDK_API_CONTRACT = "2026-05-runs-v2";
271
271
 
272
272
  // ../shared_libs/play-runtime/coordinator-headers.ts
@@ -243,7 +243,7 @@ function saveProjectDeeplineEnvValues(baseUrl, values, startDir = projectEnvStar
243
243
  }
244
244
 
245
245
  // src/version.ts
246
- var SDK_VERSION = "0.1.26";
246
+ var SDK_VERSION = "0.1.27";
247
247
  var SDK_API_CONTRACT = "2026-05-runs-v2";
248
248
 
249
249
  // ../shared_libs/play-runtime/coordinator-headers.ts
package/dist/index.d.mts CHANGED
@@ -1401,7 +1401,7 @@ declare class DeeplineClient {
1401
1401
  }>;
1402
1402
  }
1403
1403
 
1404
- declare const SDK_VERSION = "0.1.26";
1404
+ declare const SDK_VERSION = "0.1.27";
1405
1405
  declare const SDK_API_CONTRACT = "2026-05-runs-v2";
1406
1406
 
1407
1407
  /**
package/dist/index.d.ts CHANGED
@@ -1401,7 +1401,7 @@ declare class DeeplineClient {
1401
1401
  }>;
1402
1402
  }
1403
1403
 
1404
- declare const SDK_VERSION = "0.1.26";
1404
+ declare const SDK_VERSION = "0.1.27";
1405
1405
  declare const SDK_API_CONTRACT = "2026-05-runs-v2";
1406
1406
 
1407
1407
  /**
package/dist/index.js CHANGED
@@ -241,7 +241,7 @@ function resolveConfig(options) {
241
241
  }
242
242
 
243
243
  // src/version.ts
244
- var SDK_VERSION = "0.1.26";
244
+ var SDK_VERSION = "0.1.27";
245
245
  var SDK_API_CONTRACT = "2026-05-runs-v2";
246
246
 
247
247
  // ../shared_libs/play-runtime/coordinator-headers.ts
package/dist/index.mjs CHANGED
@@ -195,7 +195,7 @@ function resolveConfig(options) {
195
195
  }
196
196
 
197
197
  // src/version.ts
198
- var SDK_VERSION = "0.1.26";
198
+ var SDK_VERSION = "0.1.27";
199
199
  var SDK_API_CONTRACT = "2026-05-runs-v2";
200
200
 
201
201
  // ../shared_libs/play-runtime/coordinator-headers.ts
@@ -232,7 +232,7 @@ interface CoordinatorEnv {
232
232
  /**
233
233
  * Service binding to the long-lived Play Harness Worker
234
234
  * (apps/play-harness-worker). Provides typed RPC access to leaf-level
235
- * helpers (zod validation, runtime-API HTTP forwarder, …) that we
235
+ * helpers (runtime-API HTTP forwarder, Neon dataset IO, …) that we
236
236
  * deliberately keep OUT of every per-graphHash play bundle.
237
237
  *
238
238
  * Optional: when missing (e.g. an older deploy that hasn't been wired
@@ -547,10 +547,10 @@ const WORKFLOW_POOL_READY_POLL_MS = 250;
547
547
  const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
548
548
  const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
549
549
  const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
550
+ const WORKFLOW_POOL_START_ACK_TIMEOUT_MS = 750;
551
+ const WORKFLOW_POOL_START_ACK_POLL_MS = 25;
550
552
  const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
551
553
  const SUBMIT_INITIAL_STATE_POLL_MS = 50;
552
- const WORKFLOW_POOL_DISABLED_REASON =
553
- 'Cloudflare Workflows start runs directly; waitForEvent is reserved for real durable external waits.';
554
554
  function buildDynamicWorkflowMetadata(
555
555
  params: PlayWorkflowParams,
556
556
  ): DynamicWorkflowMetadata {
@@ -623,7 +623,7 @@ function readWorkflowTraceContext(event: unknown): {
623
623
  }
624
624
 
625
625
  function workflowPoolEnabled(): boolean {
626
- return false;
626
+ return WORKFLOW_POOL_TARGET_SIZE > 0;
627
627
  }
628
628
 
629
629
  function workflowPoolTargetSize(): number {
@@ -889,8 +889,33 @@ async function mapRunToWorkflowInstance(input: {
889
889
  env: CoordinatorEnv;
890
890
  runId: string;
891
891
  instanceId: string;
892
- }): Promise<void> {
893
- await callWorkflowPool(input.env, '/pool-map-run', {
892
+ started?: boolean;
893
+ }): Promise<boolean> {
894
+ const body = await callWorkflowPool<{ mapped?: unknown }>(
895
+ input.env,
896
+ '/pool-map-run',
897
+ {
898
+ method: 'POST',
899
+ body: JSON.stringify({
900
+ runId: input.runId,
901
+ instanceId: input.instanceId,
902
+ started: input.started === true,
903
+ version: WORKFLOW_POOL_PROTOCOL_VERSION,
904
+ }),
905
+ },
906
+ );
907
+ return body.mapped !== false;
908
+ }
909
+
910
+ async function blockWorkflowPoolRun(input: {
911
+ env: CoordinatorEnv;
912
+ runId: string;
913
+ instanceId: string;
914
+ }): Promise<{ blocked: boolean; started: boolean }> {
915
+ const body = await callWorkflowPool<{
916
+ blocked?: unknown;
917
+ started?: unknown;
918
+ }>(input.env, '/pool-block-run', {
894
919
  method: 'POST',
895
920
  body: JSON.stringify({
896
921
  runId: input.runId,
@@ -898,6 +923,82 @@ async function mapRunToWorkflowInstance(input: {
898
923
  version: WORKFLOW_POOL_PROTOCOL_VERSION,
899
924
  }),
900
925
  });
926
+ return {
927
+ blocked: body.blocked === true,
928
+ started: body.started === true,
929
+ };
930
+ }
931
+
932
+ async function readWorkflowPoolRunMapping(input: {
933
+ env: CoordinatorEnv;
934
+ runId: string;
935
+ }): Promise<{ instanceId: string | null; startedAt: number | null }> {
936
+ const body = await callWorkflowPool<{
937
+ instanceId?: unknown;
938
+ startedAt?: unknown;
939
+ }>(
940
+ input.env,
941
+ `/pool-resolve-run?runId=${encodeURIComponent(input.runId)}&version=${encodeURIComponent(
942
+ WORKFLOW_POOL_PROTOCOL_VERSION,
943
+ )}`,
944
+ ).catch(() => ({ instanceId: null, startedAt: null }));
945
+ return {
946
+ instanceId:
947
+ typeof body.instanceId === 'string' && body.instanceId
948
+ ? body.instanceId
949
+ : null,
950
+ startedAt:
951
+ typeof body.startedAt === 'number' && Number.isFinite(body.startedAt)
952
+ ? body.startedAt
953
+ : null,
954
+ };
955
+ }
956
+
957
+ async function waitForWorkflowPoolStartAck(input: {
958
+ env: CoordinatorEnv;
959
+ runId: string;
960
+ instanceId: string;
961
+ timeoutMs: number;
962
+ }): Promise<{
963
+ acknowledged: boolean;
964
+ ms: number;
965
+ polls: number;
966
+ startedAt: number | null;
967
+ mappedInstanceId: string | null;
968
+ }> {
969
+ const startedAt = Date.now();
970
+ let polls = 0;
971
+ let latestMapping: { instanceId: string | null; startedAt: number | null } = {
972
+ instanceId: null,
973
+ startedAt: null,
974
+ };
975
+ while (Date.now() - startedAt < input.timeoutMs) {
976
+ polls += 1;
977
+ latestMapping = await readWorkflowPoolRunMapping({
978
+ env: input.env,
979
+ runId: input.runId,
980
+ });
981
+ if (
982
+ latestMapping.instanceId === input.instanceId &&
983
+ latestMapping.startedAt !== null
984
+ ) {
985
+ return {
986
+ acknowledged: true,
987
+ ms: Date.now() - startedAt,
988
+ polls,
989
+ startedAt: latestMapping.startedAt,
990
+ mappedInstanceId: latestMapping.instanceId,
991
+ };
992
+ }
993
+ await sleep(WORKFLOW_POOL_START_ACK_POLL_MS);
994
+ }
995
+ return {
996
+ acknowledged: false,
997
+ ms: Date.now() - startedAt,
998
+ polls,
999
+ startedAt: latestMapping.startedAt,
1000
+ mappedInstanceId: latestMapping.instanceId,
1001
+ };
901
1002
  }
902
1003
 
903
1004
  async function resolveWorkflowInstanceIdForRun(
@@ -907,15 +1008,8 @@ async function resolveWorkflowInstanceIdForRun(
907
1008
  if (!workflowPoolEnabled()) {
908
1009
  return workflowInstanceId(runId);
909
1010
  }
910
- const body = await callWorkflowPool<{ instanceId?: unknown }>(
911
- env,
912
- `/pool-resolve-run?runId=${encodeURIComponent(runId)}&version=${encodeURIComponent(
913
- WORKFLOW_POOL_PROTOCOL_VERSION,
914
- )}`,
915
- ).catch(() => ({ instanceId: null }));
916
- return typeof body.instanceId === 'string' && body.instanceId
917
- ? body.instanceId
918
- : workflowInstanceId(runId);
1011
+ const mapping = await readWorkflowPoolRunMapping({ env, runId });
1012
+ return mapping.instanceId ? mapping.instanceId : workflowInstanceId(runId);
919
1013
  }
920
1014
 
921
1015
  async function clearWorkflowPool(env: CoordinatorEnv): Promise<number> {
@@ -927,7 +1021,10 @@ async function clearWorkflowPool(env: CoordinatorEnv): Promise<number> {
927
1021
  );
928
1022
  await Promise.all(
929
1023
  entries.map(async (entry) => {
930
- const instance = await env.PLAY_WORKFLOW.get(entry.id);
1024
+ const instance = await getWorkflowPoolInstance(env, entry.id);
1025
+ if (!instance) {
1026
+ return;
1027
+ }
931
1028
  try {
932
1029
  await instance.terminate().catch(() => undefined);
933
1030
  } finally {
@@ -942,6 +1039,27 @@ function workflowStatusName(status: InstanceStatus | null): string {
942
1039
  return typeof status?.status === 'string' ? status.status : 'unknown';
943
1040
  }
944
1041
 
1042
+ function isWorkflowInstanceNotFoundError(error: unknown): boolean {
1043
+ const message = error instanceof Error ? error.message : String(error);
1044
+ return /not[ _]found|not_found|does not exist|no such instance|404/i.test(
1045
+ message,
1046
+ );
1047
+ }
1048
+
1049
+ async function getWorkflowPoolInstance(
1050
+ env: CoordinatorEnv,
1051
+ instanceId: string,
1052
+ ): Promise<WorkflowInstance | null> {
1053
+ try {
1054
+ return await env.PLAY_WORKFLOW.get(instanceId);
1055
+ } catch (error) {
1056
+ if (isWorkflowInstanceNotFoundError(error)) {
1057
+ return null;
1058
+ }
1059
+ throw error;
1060
+ }
1061
+ }
1062
+
945
1063
  function workflowPoolStatusIsReady(statusName: string): boolean {
946
1064
  // This is only a liveness guard. Readiness itself comes from the pooled
947
1065
  // Workflow calling /pool-ready after waitForEvent("play_start") has been
@@ -1024,7 +1142,11 @@ async function refillWorkflowPoolOnce(
1024
1142
  const promotedIds: string[] = [];
1025
1143
  const removedIds: string[] = [];
1026
1144
  for (const entry of warmingEntries) {
1027
- const instance = await env.PLAY_WORKFLOW.get(entry.id);
1145
+ const instance = await getWorkflowPoolInstance(env, entry.id);
1146
+ if (!instance) {
1147
+ removedIds.push(entry.id);
1148
+ continue;
1149
+ }
1028
1150
  try {
1029
1151
  if (entry.state === 'ready' && entry.readyAt !== null) {
1030
1152
  promotedIds.push(entry.id);
@@ -1251,7 +1373,21 @@ async function submitViaPooledWorkflow(input: {
1251
1373
  return null;
1252
1374
  }
1253
1375
 
1254
- const instance = await input.env.PLAY_WORKFLOW.get(pooledInstanceId);
1376
+ const instance = await getWorkflowPoolInstance(input.env, pooledInstanceId);
1377
+ if (!instance) {
1378
+ await blockWorkflowPoolRun({
1379
+ env: input.env,
1380
+ runId: input.params.runId,
1381
+ instanceId: pooledInstanceId,
1382
+ }).catch(() => undefined);
1383
+ input.recordSubmitTiming({
1384
+ phase: 'coordinator.workflow_pool_ready_check',
1385
+ ms: Date.now() - leaseStartedAt,
1386
+ graphHash: input.params.graphHash ?? null,
1387
+ extra: { instanceId: pooledInstanceId, status: 'missing' },
1388
+ });
1389
+ return null;
1390
+ }
1255
1391
  const readyCheckStartedAt = Date.now();
1256
1392
  const status = await instance.status().catch(() => null);
1257
1393
  const statusName = workflowStatusName(status);
@@ -1262,6 +1398,11 @@ async function submitViaPooledWorkflow(input: {
1262
1398
  extra: { instanceId: pooledInstanceId, status: statusName },
1263
1399
  });
1264
1400
  if (!workflowPoolStatusIsReady(statusName)) {
1401
+ await blockWorkflowPoolRun({
1402
+ env: input.env,
1403
+ runId: input.params.runId,
1404
+ instanceId: pooledInstanceId,
1405
+ }).catch(() => undefined);
1265
1406
  await instance.terminate().catch(() => undefined);
1266
1407
  disposeRpcStub(instance);
1267
1408
  return null;
@@ -1273,6 +1414,11 @@ async function submitViaPooledWorkflow(input: {
1273
1414
  payload: buildDispatcherEnvelope(input.params),
1274
1415
  });
1275
1416
  } catch (error) {
1417
+ await blockWorkflowPoolRun({
1418
+ env: input.env,
1419
+ runId: input.params.runId,
1420
+ instanceId: pooledInstanceId,
1421
+ }).catch(() => undefined);
1276
1422
  disposeRpcStub(instance);
1277
1423
  console.warn('[coordinator.workflow_pool] sendEvent failed; falling back', {
1278
1424
  runId: input.params.runId,
@@ -1287,7 +1433,63 @@ async function submitViaPooledWorkflow(input: {
1287
1433
  graphHash: input.params.graphHash ?? null,
1288
1434
  extra: { instanceId: pooledInstanceId },
1289
1435
  });
1290
- return instance;
1436
+ const ack = await waitForWorkflowPoolStartAck({
1437
+ env: input.env,
1438
+ runId: input.params.runId,
1439
+ instanceId: pooledInstanceId,
1440
+ timeoutMs: WORKFLOW_POOL_START_ACK_TIMEOUT_MS,
1441
+ });
1442
+ if (ack.acknowledged) {
1443
+ input.recordSubmitTiming({
1444
+ phase: 'coordinator.workflow_pool_start_ack',
1445
+ ms: ack.ms,
1446
+ graphHash: input.params.graphHash ?? null,
1447
+ extra: {
1448
+ acknowledged: true,
1449
+ instanceId: pooledInstanceId,
1450
+ polls: ack.polls,
1451
+ startedAt: ack.startedAt,
1452
+ },
1453
+ });
1454
+ return instance;
1455
+ }
1456
+
1457
+ const blockStartedAt = Date.now();
1458
+ const block = await blockWorkflowPoolRun({
1459
+ env: input.env,
1460
+ runId: input.params.runId,
1461
+ instanceId: pooledInstanceId,
1462
+ }).catch(() => ({ blocked: false, started: false }));
1463
+ input.recordSubmitTiming({
1464
+ phase: 'coordinator.workflow_pool_start_ack',
1465
+ ms: ack.ms,
1466
+ graphHash: input.params.graphHash ?? null,
1467
+ extra: {
1468
+ acknowledged: block.started,
1469
+ instanceId: pooledInstanceId,
1470
+ polls: ack.polls,
1471
+ startedAt: ack.startedAt,
1472
+ mappedInstanceId: ack.mappedInstanceId,
1473
+ blocked: block.blocked,
1474
+ blockMs: Date.now() - blockStartedAt,
1475
+ },
1476
+ });
1477
+ if (block.started) {
1478
+ return instance;
1479
+ }
1480
+ await instance.terminate().catch(() => undefined);
1481
+ disposeRpcStub(instance);
1482
+ input.recordSubmitTiming({
1483
+ phase: 'coordinator.workflow_pool_fallback',
1484
+ ms: Date.now() - sendStartedAt,
1485
+ graphHash: input.params.graphHash ?? null,
1486
+ extra: {
1487
+ reason: 'start_ack_timeout',
1488
+ instanceId: pooledInstanceId,
1489
+ ackTimeoutMs: WORKFLOW_POOL_START_ACK_TIMEOUT_MS,
1490
+ },
1491
+ });
1492
+ return null;
1291
1493
  }
1292
1494
 
1293
1495
  function readWorkflowPayload(event: unknown): Record<string, unknown> | null {
@@ -2186,9 +2388,35 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
2186
2388
  dispatchedEvent = {
2187
2389
  payload: startEvent.payload,
2188
2390
  timestamp: startEvent.timestamp,
2189
- instanceId: workflowEvent.instanceId,
2391
+ instanceId: workflowEvent.instanceId ?? pooledPayload.poolId,
2190
2392
  };
2191
2393
  const dispatchedTrace = readWorkflowTraceContext(dispatchedEvent);
2394
+ const mapped = await mapRunToWorkflowInstance({
2395
+ env: this.env,
2396
+ runId: dispatchedTrace.runId,
2397
+ instanceId: pooledPayload.poolId,
2398
+ started: true,
2399
+ }).catch((error) => {
2400
+ console.warn('[coordinator.workflow_pool] start ack failed', {
2401
+ poolId: pooledPayload.poolId,
2402
+ runId: dispatchedTrace.runId,
2403
+ message: error instanceof Error ? error.message : String(error),
2404
+ });
2405
+ return false;
2406
+ });
2407
+ if (!mapped) {
2408
+ trace({
2409
+ runId: dispatchedTrace.runId,
2410
+ phase: 'coordinator.workflow_pool_start_blocked',
2411
+ ms: 0,
2412
+ graphHash: dispatchedTrace.graphHash,
2413
+ extra: {
2414
+ instanceId: pooledPayload.poolId,
2415
+ eventType: startEvent.type,
2416
+ },
2417
+ });
2418
+ return { ok: false, blocked: true, runId: dispatchedTrace.runId };
2419
+ }
2192
2420
  const eventDeliveryMs = Math.max(
2193
2421
  0,
2194
2422
  Date.now() - startEvent.timestamp.getTime(),
@@ -2472,7 +2700,15 @@ const coordinatorEntrypoint = {
2472
2700
  const entries = await listWorkflowPoolEntries(env);
2473
2701
  const detailed = [];
2474
2702
  for (const entry of entries) {
2475
- const instance = await env.PLAY_WORKFLOW.get(entry.id);
2703
+ const instance = await getWorkflowPoolInstance(env, entry.id);
2704
+ if (!instance) {
2705
+ detailed.push({
2706
+ ...entry,
2707
+ status: 'missing',
2708
+ mappedStatus: 'failed',
2709
+ });
2710
+ continue;
2711
+ }
2476
2712
  try {
2477
2713
  const status = await instance.status().catch(() => null);
2478
2714
  detailed.push({
@@ -2536,6 +2772,14 @@ const coordinatorEntrypoint = {
2536
2772
  async tail(events: unknown[], env: CoordinatorEnv): Promise<void> {
2537
2773
  await flushTailRunLogs(events, env);
2538
2774
  },
2775
+ async scheduled(
2776
+ _controller: unknown,
2777
+ env: CoordinatorEnv,
2778
+ ctx?: ExecutionContext,
2779
+ ): Promise<void> {
2780
+ if (!workflowPoolEnabled()) return;
2781
+ ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
2782
+ },
2539
2783
  };
2540
2784
 
2541
2785
  export default coordinatorEntrypoint;
@@ -2698,36 +2942,100 @@ async function handleWorkflowRoute(input: {
2698
2942
  'Start apps/play-harness-worker before the coordinator or fix wrangler.toml services.',
2699
2943
  );
2700
2944
  }
2945
+ const preloadedDbSessions = Array.isArray(params.preloadedDbSessions)
2946
+ ? params.preloadedDbSessions
2947
+ : [];
2948
+ if (preloadedDbSessions.length > 0 && params.executorToken) {
2949
+ const prewarmStartedAt = Date.now();
2950
+ recordSubmitTiming({
2951
+ phase: 'coordinator.harness_prewarm_postgres_start',
2952
+ ms: 0,
2953
+ graphHash: params.graphHash ?? null,
2954
+ extra: { sessions: preloadedDbSessions.length },
2955
+ });
2956
+ const prewarmPromise = env.HARNESS.prewarmPostgresSessions({
2957
+ executorToken: params.executorToken,
2958
+ sessions: preloadedDbSessions,
2959
+ })
2960
+ .then((result) => {
2961
+ recordSubmitTiming({
2962
+ phase: 'coordinator.harness_prewarm_postgres',
2963
+ ms: Date.now() - prewarmStartedAt,
2964
+ graphHash: params.graphHash ?? null,
2965
+ extra: {
2966
+ status: 'ok',
2967
+ sessions: result.sessions,
2968
+ },
2969
+ });
2970
+ })
2971
+ .catch((error: unknown) => {
2972
+ recordSubmitTiming({
2973
+ phase: 'coordinator.harness_prewarm_postgres',
2974
+ ms: Date.now() - prewarmStartedAt,
2975
+ graphHash: params.graphHash ?? null,
2976
+ extra: {
2977
+ status: 'failed',
2978
+ sessions: preloadedDbSessions.length,
2979
+ error: error instanceof Error ? error.message : String(error),
2980
+ },
2981
+ });
2982
+ });
2983
+ input.ctx?.waitUntil(prewarmPromise);
2984
+ }
2701
2985
  let instance: WorkflowInstance | null = null;
2702
2986
  try {
2703
- const dispatchStartedAt = Date.now();
2987
+ const statusEventStartedAt = Date.now();
2988
+ await appendCoordinatorRunEvent(env, {
2989
+ runId: submittedRunId,
2990
+ type: 'status',
2991
+ status: 'running',
2992
+ ts: Date.now(),
2993
+ });
2704
2994
  recordSubmitTiming({
2705
- phase: 'coordinator.workflow_pool_attempt',
2706
- ms: 0,
2995
+ phase: 'coordinator.submit_status_event',
2996
+ ms: Date.now() - statusEventStartedAt,
2707
2997
  graphHash: params.graphHash ?? null,
2708
- extra: {
2709
- usedPool: false,
2710
- disabled: true,
2711
- reason: WORKFLOW_POOL_DISABLED_REASON,
2712
- },
2713
2998
  });
2714
- const createStartedAt = Date.now();
2715
- instance = await createDynamicWorkflowInstance({
2999
+ const dispatchStartedAt = Date.now();
3000
+ const poolAttemptStartedAt = Date.now();
3001
+ instance = await submitViaPooledWorkflow({
2716
3002
  env,
2717
- id: defaultInstanceId,
2718
3003
  params,
3004
+ recordSubmitTiming,
2719
3005
  });
2720
3006
  recordSubmitTiming({
2721
- phase: 'coordinator.workflow_create',
2722
- ms: Date.now() - createStartedAt,
3007
+ phase: 'coordinator.workflow_pool_attempt',
3008
+ ms: Date.now() - poolAttemptStartedAt,
2723
3009
  graphHash: params.graphHash ?? null,
2724
- extra: { instanceId: instance.id },
3010
+ extra: {
3011
+ usedPool: Boolean(instance),
3012
+ enabled: workflowPoolEnabled(),
3013
+ },
2725
3014
  });
3015
+ if (!instance) {
3016
+ const createStartedAt = Date.now();
3017
+ instance = await createDynamicWorkflowInstance({
3018
+ env,
3019
+ id: defaultInstanceId,
3020
+ params,
3021
+ });
3022
+ recordSubmitTiming({
3023
+ phase: 'coordinator.workflow_create',
3024
+ ms: Date.now() - createStartedAt,
3025
+ graphHash: params.graphHash ?? null,
3026
+ extra: { instanceId: instance.id },
3027
+ });
3028
+ }
2726
3029
  recordSubmitTiming({
2727
3030
  phase: 'coordinator.dispatch_workflow',
2728
3031
  ms: Date.now() - dispatchStartedAt,
2729
3032
  graphHash: params.graphHash ?? null,
2730
- extra: { startMode: 'direct_workflow_create' },
3033
+ extra: {
3034
+ startMode:
3035
+ instance.id === defaultInstanceId
3036
+ ? 'direct_workflow_create'
3037
+ : 'pooled_workflow_start_event',
3038
+ },
2731
3039
  });
2732
3040
  const initialWaitMsRaw = Number(
2733
3041
  new URL(request.url).searchParams.get('initialWaitMs') ?? '0',
@@ -2761,6 +3069,9 @@ async function handleWorkflowRoute(input: {
2761
3069
  ms: totalMs,
2762
3070
  graphHash: params.graphHash ?? null,
2763
3071
  });
3072
+ if (workflowPoolEnabled() && instance.id === defaultInstanceId) {
3073
+ input.ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
3074
+ }
2764
3075
  return Response.json({
2765
3076
  runId,
2766
3077
  status: 'submitted',
@@ -3016,11 +3327,19 @@ async function handleWorkflowRoute(input: {
3016
3327
  afterSeq,
3017
3328
  timeoutMs: waitMs,
3018
3329
  }).catch(() => null);
3330
+ const rawEvents = eventResult?.events ?? [];
3331
+ const terminalEventIndex = rawEvents.findIndex(
3332
+ (event) => event.type === 'terminal',
3333
+ );
3334
+ const events =
3335
+ terminalEventIndex >= 0
3336
+ ? rawEvents.slice(0, terminalEventIndex + 1)
3337
+ : rawEvents;
3019
3338
  const coordinatorTrace =
3020
- includeTrace && eventResult?.events.length
3339
+ includeTrace && events.length
3021
3340
  ? await listCoordinatorPerfTrace(env, runId).catch(() => [])
3022
3341
  : [];
3023
- const terminalEvent = eventResult?.events.find(
3342
+ const terminalEvent = events.find(
3024
3343
  (event): event is Extract<CoordinatorRunEvent, { type: 'terminal' }> =>
3025
3344
  event.type === 'terminal',
3026
3345
  );
@@ -3036,7 +3355,7 @@ async function handleWorkflowRoute(input: {
3036
3355
  completedAt: terminalEvent.ts,
3037
3356
  liveLogs: sanitizeLiveLogLines(terminalEvent.liveLogs),
3038
3357
  liveNodeProgress: terminalEvent.liveNodeProgress ?? null,
3039
- events: eventResult?.events ?? [],
3358
+ events,
3040
3359
  latestSeq: eventResult?.latestSeq ?? afterSeq,
3041
3360
  wait: null,
3042
3361
  coordinatorObserve: {
@@ -3052,7 +3371,7 @@ async function handleWorkflowRoute(input: {
3052
3371
  return Response.json({
3053
3372
  runId,
3054
3373
  status: 'running',
3055
- events: eventResult?.events ?? [],
3374
+ events,
3056
3375
  latestSeq: eventResult?.latestSeq ?? afterSeq,
3057
3376
  wait: null,
3058
3377
  coordinatorObserve: {
@@ -3250,7 +3569,7 @@ function stableHash(value: string): string {
3250
3569
  return (hash >>> 0).toString(36);
3251
3570
  }
3252
3571
 
3253
- const DYNAMIC_PLAY_WORKER_HARNESS_VERSION = 'h11-full-row-dataset-handles';
3572
+ const DYNAMIC_PLAY_WORKER_HARNESS_VERSION = 'h16-coordinator-only-prewarm';
3254
3573
  const DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES = 64;
3255
3574
  const dynamicWorkerBundledCodeCache = new Map<string, string>();
3256
3575
 
@@ -106,7 +106,6 @@ import {
106
106
  // re-bundle harness internals into per-play. Keep that in mind.
107
107
  import {
108
108
  harnessPersistCompletedSheetRows,
109
- harnessPrewarmPostgresSessions,
110
109
  harnessReadSheetDatasetRows,
111
110
  harnessReadStagedFileChunk,
112
111
  harnessStartSheetDataset,
@@ -568,7 +567,11 @@ async function postRuntimeApi<T>(
568
567
  // hits the same handler with the same auth — only the transport changes.
569
568
  const serializedBody = JSON.stringify(body);
570
569
  let lastError: unknown = null;
571
- for (let attempt = 0; attempt <= RUNTIME_API_RETRY_DELAYS_MS.length; attempt += 1) {
570
+ for (
571
+ let attempt = 0;
572
+ attempt <= RUNTIME_API_RETRY_DELAYS_MS.length;
573
+ attempt += 1
574
+ ) {
572
575
  let res: Response;
573
576
  try {
574
577
  res = await fetchRuntimeApi(baseUrl, '/api/v2/plays/internal/runtime', {
@@ -618,7 +621,13 @@ function isRetryableRuntimeApiError(error: unknown): boolean {
618
621
  }
619
622
 
620
623
  function isRetryableRuntimeApiResponse(status: number, body: string): boolean {
621
- if (status === 408 || status === 429 || status === 502 || status === 503 || status === 504) {
624
+ if (
625
+ status === 408 ||
626
+ status === 429 ||
627
+ status === 502 ||
628
+ status === 503 ||
629
+ status === 504
630
+ ) {
622
631
  return true;
623
632
  }
624
633
  return (
@@ -2465,7 +2474,10 @@ function readHarnessStagedFileChunks(input: {
2465
2474
  }
2466
2475
 
2467
2476
  const requiredBytes = expectedBytes ?? objectSize;
2468
- if (typeof requiredBytes === 'number' && observedBytes !== requiredBytes) {
2477
+ if (
2478
+ typeof requiredBytes === 'number' &&
2479
+ observedBytes !== requiredBytes
2480
+ ) {
2469
2481
  recordRunnerPerfTrace({
2470
2482
  req: input.req,
2471
2483
  phase: 'csv.read_mismatch',
@@ -2761,6 +2773,28 @@ async function prepareMapRows(input: {
2761
2773
  userEmail: input.req.userEmail,
2762
2774
  preloadedDbSessions: input.req.preloadedDbSessions ?? null,
2763
2775
  });
2776
+ for (const timing of result.timings ?? []) {
2777
+ const phase =
2778
+ typeof timing.phase === 'string' && timing.phase.trim()
2779
+ ? timing.phase.trim()
2780
+ : 'unknown';
2781
+ const ms =
2782
+ typeof timing.ms === 'number' && Number.isFinite(timing.ms)
2783
+ ? timing.ms
2784
+ : 0;
2785
+ const { phase: _phase, ms: _ms, ...extra } = timing;
2786
+ void _phase;
2787
+ void _ms;
2788
+ recordRunnerPerfTrace({
2789
+ req: input.req,
2790
+ phase: `sheet_start.${phase}`,
2791
+ ms,
2792
+ extra: {
2793
+ tableNamespace: input.tableNamespace,
2794
+ ...extra,
2795
+ },
2796
+ });
2797
+ }
2764
2798
  return {
2765
2799
  inserted: result.inserted,
2766
2800
  skipped: result.skipped,
@@ -3717,7 +3751,8 @@ function createMinimalWorkerCtx(
3717
3751
  if (matchByPath) {
3718
3752
  file = {
3719
3753
  logicalPath: matchByPath.playPath,
3720
- fileName: matchByPath.playPath.split('/').pop() ?? matchByPath.playPath,
3754
+ fileName:
3755
+ matchByPath.playPath.split('/').pop() ?? matchByPath.playPath,
3721
3756
  storageKey: matchByPath.storageKey,
3722
3757
  contentType: matchByPath.contentType,
3723
3758
  bytes: matchByPath.bytes,
@@ -4245,19 +4280,6 @@ async function executeRunRequest(
4245
4280
  });
4246
4281
  const abortController = options?.abortController ?? new AbortController();
4247
4282
  const abortSignal = abortController.signal;
4248
- const postgresPrewarmStartedAt = nowMs();
4249
- await harnessPrewarmPostgresSessions({
4250
- executorToken: req.executorToken,
4251
- sessions: req.preloadedDbSessions ?? [],
4252
- });
4253
- recordRunnerPerfTrace({
4254
- req,
4255
- phase: 'runner.prewarm_postgres',
4256
- ms: nowMs() - postgresPrewarmStartedAt,
4257
- extra: {
4258
- sessions: req.preloadedDbSessions?.length ?? 0,
4259
- },
4260
- });
4261
4283
  let runLogBuffer: string[] = [];
4262
4284
  let pendingRunLogLines: string[] = [];
4263
4285
  let stepProgressByNodeId: LiveNodeProgressMap = {};
@@ -4427,6 +4449,11 @@ async function executeRunRequest(
4427
4449
  if (!options?.persistResultDatasets) return;
4428
4450
  await ledgerFlushInFlight.catch(() => undefined);
4429
4451
  const now = nowMs();
4452
+ pendingRunLogLines = runLogBuffer;
4453
+ dirtyProgressNodeIds = new Set([
4454
+ ...dirtyProgressNodeIds,
4455
+ ...Object.keys(stepProgressByNodeId),
4456
+ ]);
4430
4457
  pendingLedgerEvents = [...pendingLedgerEvents, terminalEvent];
4431
4458
  const events = drainPendingLedgerEvents(now);
4432
4459
  if (events.length === 0) return;
@@ -4510,12 +4537,12 @@ async function executeRunRequest(
4510
4537
  ms: nowMs() - serializeStartedAt,
4511
4538
  });
4512
4539
  if (options?.persistResultDatasets) {
4513
- const persistStartedAt = nowMs();
4540
+ const ledgerFlushWaitStartedAt = nowMs();
4514
4541
  await ledgerFlushInFlight.catch(() => undefined);
4515
4542
  recordRunnerPerfTrace({
4516
4543
  req,
4517
4544
  phase: 'runner.run_ledger_flush_wait',
4518
- ms: nowMs() - persistStartedAt,
4545
+ ms: nowMs() - ledgerFlushWaitStartedAt,
4519
4546
  });
4520
4547
  const resultDatasetStartedAt = nowMs();
4521
4548
  await persistResultDatasets(req, result, serializedResult);
@@ -4526,31 +4553,58 @@ async function executeRunRequest(
4526
4553
  });
4527
4554
  const terminalResult = trimResultForStatus(serializedResult);
4528
4555
  const terminalOccurredAt = nowMs();
4556
+ const terminalLedgerPromise = (async () => {
4557
+ const terminalUpdateStartedAt = nowMs();
4558
+ await flushTerminalLedgerEvents({
4559
+ type: 'run.completed',
4560
+ runId: req.runId,
4561
+ source: 'worker',
4562
+ occurredAt: terminalOccurredAt,
4563
+ result: terminalResult,
4564
+ });
4565
+ recordRunnerPerfTrace({
4566
+ req,
4567
+ phase: 'runner.terminal_ledger_append',
4568
+ ms: nowMs() - terminalUpdateStartedAt,
4569
+ });
4570
+ })().catch((error) => {
4571
+ console.error(
4572
+ `[play-harness] non-fatal terminal ledger append failed runId=${req.runId}: ${
4573
+ error instanceof Error ? error.message : String(error)
4574
+ }`,
4575
+ );
4576
+ });
4577
+
4578
+ await terminalLedgerPromise;
4579
+
4529
4580
  const billingStartedAt = nowMs();
4530
- await finalizeWorkerComputeBilling({
4581
+ const billingPromise = finalizeWorkerComputeBilling({
4531
4582
  req,
4532
4583
  success: true,
4533
4584
  actionEstimate: 4,
4585
+ }).then(() => {
4586
+ recordRunnerPerfTrace({
4587
+ req,
4588
+ phase: 'runner.compute_billing_finalize',
4589
+ ms: nowMs() - billingStartedAt,
4590
+ });
4534
4591
  });
4535
- recordRunnerPerfTrace({
4536
- req,
4537
- phase: 'runner.compute_billing_finalize',
4538
- ms: nowMs() - billingStartedAt,
4539
- });
4540
-
4541
- const terminalUpdateStartedAt = nowMs();
4542
- await flushTerminalLedgerEvents({
4543
- type: 'run.completed',
4544
- runId: req.runId,
4545
- source: 'worker',
4546
- occurredAt: terminalOccurredAt,
4547
- result: terminalResult,
4548
- });
4549
- recordRunnerPerfTrace({
4550
- req,
4551
- phase: 'runner.terminal_ledger_append',
4552
- ms: nowMs() - terminalUpdateStartedAt,
4553
- });
4592
+ if (extractMaxCreditsPerRun(req.contractSnapshot) !== null) {
4593
+ await billingPromise;
4594
+ } else {
4595
+ const nonBlockingBillingPromise = billingPromise.catch((error) => {
4596
+ console.error(
4597
+ `[play-harness] non-fatal compute billing finalize failed runId=${req.runId}: ${
4598
+ error instanceof Error ? error.message : String(error)
4599
+ }`,
4600
+ );
4601
+ });
4602
+ if (options?.waitUntil) {
4603
+ options.waitUntil(nonBlockingBillingPromise);
4604
+ } else {
4605
+ await nonBlockingBillingPromise;
4606
+ }
4607
+ }
4554
4608
  }
4555
4609
  const parentSignalStartedAt = nowMs();
4556
4610
  await signalParentPlayTerminal({
@@ -4713,7 +4767,9 @@ function runRequestFromWorkflowParams(
4713
4767
  inputFile && inputStorageKey
4714
4768
  ? {
4715
4769
  [fileName]: {
4716
- logicalPath: String(inputFile.logicalPath ?? inputFile.path ?? fileName),
4770
+ logicalPath: String(
4771
+ inputFile.logicalPath ?? inputFile.path ?? fileName,
4772
+ ),
4717
4773
  fileName,
4718
4774
  storageKey: inputStorageKey,
4719
4775
  contentType:
@@ -5255,7 +5311,10 @@ function inferOutputRows(result: unknown): number {
5255
5311
  }
5256
5312
  if (typeof value !== 'object') return;
5257
5313
  const record = value as Record<string, unknown>;
5258
- if (typeof record.tableNamespace === 'string' && typeof record.count === 'number') {
5314
+ if (
5315
+ typeof record.tableNamespace === 'string' &&
5316
+ typeof record.count === 'number'
5317
+ ) {
5259
5318
  datasets.push(record.count);
5260
5319
  }
5261
5320
  for (const [key, child] of Object.entries(record)) {
@@ -8,13 +8,11 @@
8
8
  *
9
9
  * What it does:
10
10
  * - Exposes thin functions that look like the in-bundle implementations
11
- * they're replacing (validate, runtime-api call, …).
11
+ * they're replacing (runtime-api call, staged dataset IO, …).
12
12
  * - Each function calls `env.HARNESS.<method>(...)` — the typed RPC
13
13
  * stub provided by the Cloudflare service binding.
14
14
  *
15
15
  * What it does NOT do:
16
- * - Does NOT import zod (that's the whole point — zod stays in the
17
- * harness Worker).
18
16
  * - Does NOT import the harness Worker's `PlayHarness` class (that
19
17
  * would re-bundle the whole harness; only TYPES are imported).
20
18
  * - Does NOT cache RPC results across plays — caching belongs to
@@ -36,12 +34,10 @@ import type {
36
34
  PreloadedRuntimeDbSessionInput,
37
35
  RuntimeApiCallInput,
38
36
  RuntimeApiCallResult,
39
- RuntimePayloadSchemaId,
40
37
  SheetDatasetRowsInput,
41
38
  SheetDatasetRowsResult,
42
39
  StagedFileChunkInput,
43
40
  StagedFileChunkResult,
44
- ValidatePayloadResult,
45
41
  } from '../../../apps/play-harness-worker/src/rpc-types';
46
42
 
47
43
  /**
@@ -173,6 +169,7 @@ export async function harnessStartSheetDataset(input: {
173
169
  pendingRows: Array<Record<string, unknown>>;
174
170
  completedRows: Array<Record<string, unknown>>;
175
171
  tableNamespace: string;
172
+ timings?: Array<Record<string, unknown>>;
176
173
  }> {
177
174
  return requireBinding().startSheetDataset(input);
178
175
  }
@@ -195,18 +192,3 @@ export async function harnessPersistCompletedSheetRows(input: {
195
192
  }): Promise<{ ok: true; rowsWritten: number; tableNamespace: string }> {
196
193
  return requireBinding().persistCompletedMapRows(input);
197
194
  }
198
-
199
- /**
200
- * Validate a payload against a named schema. The schema definitions
201
- * (and zod itself) live in the harness Worker — see
202
- * `apps/play-harness-worker/src/leaves/validate.ts → KNOWN_SCHEMAS`.
203
- *
204
- * Use a schema id of the form `tool:<provider>:<operation>` for tool
205
- * inputs, or `runtime-api:<action>` for runtime-api request bodies.
206
- */
207
- export async function harnessValidatePayload(
208
- schemaId: RuntimePayloadSchemaId,
209
- payload: unknown,
210
- ): Promise<ValidatePayloadResult> {
211
- return requireBinding().validatePayload({ schemaId, payload });
212
- }
@@ -1,2 +1,2 @@
1
- export const SDK_VERSION = "0.1.26";
1
+ export const SDK_VERSION = "0.1.27";
2
2
  export const SDK_API_CONTRACT = "2026-05-runs-v2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {