deepline 0.1.168 → 0.1.169

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 (30) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +317 -26
  2. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +99 -6
  3. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +229 -75
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +119 -33
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +2 -0
  6. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-instance-create.ts +3 -0
  7. package/dist/bundling-sources/sdk/src/client.ts +29 -1
  8. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  9. package/dist/bundling-sources/sdk/src/types.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/play-data-plane/column-names.ts +50 -8
  11. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +40 -1
  12. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +3 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
  19. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +202 -45
  20. package/dist/cli/index.js +70 -113
  21. package/dist/cli/index.mjs +70 -113
  22. package/dist/{compiler-manifest-VhtM9n24.d.mts → compiler-manifest-OwORQ07f.d.mts} +1 -0
  23. package/dist/{compiler-manifest-VhtM9n24.d.ts → compiler-manifest-OwORQ07f.d.ts} +1 -0
  24. package/dist/index.d.mts +5 -1
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +26 -5
  27. package/dist/index.mjs +26 -5
  28. package/dist/plays/bundle-play-file.d.mts +2 -2
  29. package/dist/plays/bundle-play-file.d.ts +2 -2
  30. package/package.json +1 -1
@@ -112,6 +112,7 @@ export type PlayWorkflowParams = {
112
112
  dynamicWorkerCode?: string | null;
113
113
  executorToken: string;
114
114
  baseUrl: string;
115
+ integrationMode?: 'live' | 'eval_stub' | 'fixture' | null;
115
116
  orgId: string;
116
117
  userEmail: string;
117
118
  userId?: string | null;
@@ -628,6 +629,7 @@ type DynamicWorkflowMetadata = {
628
629
  artifactStorageKey: string;
629
630
  artifactHash?: string | null;
630
631
  dynamicWorkerCode?: string | null;
632
+ integrationMode?: 'live' | 'eval_stub' | 'fixture' | null;
631
633
  packagedFiles?: Array<{
632
634
  playPath: string;
633
635
  storageKey: string;
@@ -640,9 +642,19 @@ type DynamicWorkflowMetadata = {
640
642
  const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
641
643
  const SUBMIT_INITIAL_STATE_POLL_MS = 50;
642
644
  const WORKFLOW_RETRY_STATE_TTL_MS = 60 * 60 * 1000;
645
+ const WORKFLOW_SUBMIT_DUPLICATE_WAIT_MS = 55_000;
646
+ const WORKFLOW_SUBMIT_DUPLICATE_POLL_MS = 250;
643
647
  const COORDINATOR_WORKER_MEMORY_LIMIT_MESSAGE =
644
648
  '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.';
645
649
 
650
+ function normalizeIntegrationMode(
651
+ value: unknown,
652
+ ): 'live' | 'eval_stub' | 'fixture' | null {
653
+ return value === 'live' || value === 'eval_stub' || value === 'fixture'
654
+ ? value
655
+ : null;
656
+ }
657
+
646
658
  function buildDynamicWorkflowMetadata(
647
659
  params: PlayWorkflowParams,
648
660
  ): DynamicWorkflowMetadata {
@@ -652,6 +664,7 @@ function buildDynamicWorkflowMetadata(
652
664
  artifactStorageKey: params.artifactStorageKey,
653
665
  artifactHash: params.artifactHash ?? null,
654
666
  dynamicWorkerCode: params.dynamicWorkerCode ?? null,
667
+ integrationMode: normalizeIntegrationMode(params.integrationMode),
655
668
  packagedFiles: normalizePackagedFiles(params.packagedFiles),
656
669
  };
657
670
  }
@@ -879,6 +892,72 @@ async function resolveWorkflowInstanceIdForRun(
879
892
  : workflowInstanceId(runId);
880
893
  }
881
894
 
895
+ async function claimWorkflowSubmit(input: {
896
+ env: CoordinatorEnv;
897
+ runId: string;
898
+ }): Promise<{
899
+ claimed: boolean;
900
+ status: 'creating' | 'created' | null;
901
+ instanceId: string | null;
902
+ }> {
903
+ const body = await callRunScopedControl<{
904
+ claimed?: unknown;
905
+ status?: unknown;
906
+ instanceId?: unknown;
907
+ }>(input.env, input.runId, '/workflow-submit-claim', {
908
+ method: 'POST',
909
+ body: JSON.stringify({
910
+ runId: input.runId,
911
+ ttlMs: WORKFLOW_RETRY_STATE_TTL_MS,
912
+ }),
913
+ });
914
+ return {
915
+ claimed: body.claimed === true,
916
+ status:
917
+ body.status === 'creating' || body.status === 'created'
918
+ ? body.status
919
+ : null,
920
+ instanceId: typeof body.instanceId === 'string' ? body.instanceId : null,
921
+ };
922
+ }
923
+
924
+ async function releaseWorkflowSubmitClaim(input: {
925
+ env: CoordinatorEnv;
926
+ runId: string;
927
+ }): Promise<void> {
928
+ await callRunScopedControl<{ ok?: unknown }>(
929
+ input.env,
930
+ input.runId,
931
+ '/workflow-submit-release',
932
+ {
933
+ method: 'POST',
934
+ body: JSON.stringify({ runId: input.runId }),
935
+ },
936
+ );
937
+ }
938
+
939
+ async function waitForWorkflowSubmitCreated(input: {
940
+ env: CoordinatorEnv;
941
+ runId: string;
942
+ initialClaim: Awaited<ReturnType<typeof claimWorkflowSubmit>>;
943
+ timeoutMs: number;
944
+ pollMs: number;
945
+ }): Promise<Awaited<ReturnType<typeof claimWorkflowSubmit>>> {
946
+ let claim = input.initialClaim;
947
+ const deadline = Date.now() + input.timeoutMs;
948
+ while (claim.status !== 'created' || !claim.instanceId) {
949
+ if (Date.now() >= deadline) {
950
+ return claim;
951
+ }
952
+ await sleep(input.pollMs);
953
+ claim = await claimWorkflowSubmit({
954
+ env: input.env,
955
+ runId: input.runId,
956
+ });
957
+ }
958
+ return claim;
959
+ }
960
+
882
961
  function assertEncryptedPreloadedDbSessions(
883
962
  sessions: PreloadedRuntimeDbSession[],
884
963
  ): void {
@@ -1715,7 +1794,7 @@ async function preloadChildRuntimeDbSessions(input: {
1715
1794
  return sessions;
1716
1795
  }
1717
1796
 
1718
- async function registerInlineChildRunWithRuntime(input: {
1797
+ async function registerChildRunWithRuntime(input: {
1719
1798
  env: CoordinatorEnv;
1720
1799
  baseUrl: string;
1721
1800
  childExecutorToken: string;
@@ -1723,6 +1802,9 @@ async function registerInlineChildRunWithRuntime(input: {
1723
1802
  childPlayName: string;
1724
1803
  manifest: PlayRuntimeManifest;
1725
1804
  governance: PlayCallGovernanceSnapshot;
1805
+ runtimeBackend: string;
1806
+ schedulerBackend: string;
1807
+ executionProfile: string;
1726
1808
  }): Promise<void> {
1727
1809
  const response = await input.env.HARNESS.runtimeApiCall({
1728
1810
  executorToken: input.childExecutorToken,
@@ -1743,9 +1825,9 @@ async function registerInlineChildRunWithRuntime(input: {
1743
1825
  artifactStorageKey: input.manifest.artifactStorageKey,
1744
1826
  artifactHash: input.manifest.artifactHash,
1745
1827
  graphHash: input.manifest.graphHash,
1746
- runtimeBackend: 'workers_edge',
1747
- schedulerBackend: 'inline_child',
1748
- executionProfile: 'workers_edge',
1828
+ runtimeBackend: input.runtimeBackend,
1829
+ schedulerBackend: input.schedulerBackend,
1830
+ executionProfile: input.executionProfile,
1749
1831
  ...(typeof input.manifest.maxCreditsPerRun === 'number'
1750
1832
  ? { maxCreditsPerRun: input.manifest.maxCreditsPerRun }
1751
1833
  : {}),
@@ -1761,6 +1843,43 @@ async function registerInlineChildRunWithRuntime(input: {
1761
1843
  }
1762
1844
  }
1763
1845
 
1846
+ async function markRegisteredChildRunFailed(input: {
1847
+ env: CoordinatorEnv;
1848
+ baseUrl: string;
1849
+ childExecutorToken: string;
1850
+ childRunId: string;
1851
+ error: unknown;
1852
+ }): Promise<void> {
1853
+ const message =
1854
+ input.error instanceof Error ? input.error.message : String(input.error);
1855
+ const response = await input.env.HARNESS.runtimeApiCall({
1856
+ executorToken: input.childExecutorToken,
1857
+ baseUrl: input.baseUrl,
1858
+ path: '/api/v2/plays/internal/runtime',
1859
+ headers: { 'x-deepline-request-id': crypto.randomUUID() },
1860
+ timeoutMs: 15_000,
1861
+ body: {
1862
+ action: 'append_run_events',
1863
+ playId: input.childRunId,
1864
+ events: [
1865
+ {
1866
+ type: 'run.failed',
1867
+ runId: input.childRunId,
1868
+ source: 'coordinator',
1869
+ occurredAt: Date.now(),
1870
+ error: `Child workflow submit failed: ${message}`,
1871
+ } satisfies PlayRunLedgerEvent,
1872
+ ],
1873
+ },
1874
+ });
1875
+ if (response.status < 200 || response.status >= 300) {
1876
+ const text = response.body ?? '';
1877
+ throw new Error(
1878
+ `Inline child run failure mark failed ${response.status}: ${text.slice(0, 800)}`,
1879
+ );
1880
+ }
1881
+ }
1882
+
1764
1883
  type CoordinatorRuntimeApiTiming = {
1765
1884
  phase: string;
1766
1885
  ms: number;
@@ -2094,6 +2213,7 @@ function buildChildWorkflowParams(input: {
2094
2213
  dynamicWorkerCode,
2095
2214
  executorToken: childToken,
2096
2215
  baseUrl,
2216
+ integrationMode: normalizeIntegrationMode(body.integrationMode),
2097
2217
  orgId,
2098
2218
  userEmail: typeof body.userEmail === 'string' ? body.userEmail : '',
2099
2219
  userId: typeof body.userId === 'string' ? body.userId : null,
@@ -2121,6 +2241,7 @@ function runRequestFromPlayWorkflowParams(
2121
2241
  callbackUrl: params.baseUrl,
2122
2242
  executorToken: params.executorToken,
2123
2243
  baseUrl: params.baseUrl,
2244
+ integrationMode: normalizeIntegrationMode(params.integrationMode),
2124
2245
  orgId: params.orgId,
2125
2246
  playName: params.playName,
2126
2247
  graphHash: params.graphHash,
@@ -2630,22 +2751,63 @@ async function submitChildWorkflowThroughCoordinator(input: {
2630
2751
  preloadedDbSessions.length > 0 ? preloadedDbSessions : null,
2631
2752
  });
2632
2753
 
2633
- const workflowSubmitStartedAt = Date.now();
2634
- const response = await handleWorkflowRoute({
2635
- runId: childRunId,
2636
- action: 'submit',
2637
- request: new Request(
2638
- `https://deepline.coordinator.internal/workflow/${encodeURIComponent(
2639
- childRunId,
2640
- )}/submit`,
2641
- {
2642
- method: 'POST',
2643
- headers: { 'content-type': 'application/json' },
2644
- body: JSON.stringify(params),
2645
- },
2646
- ),
2754
+ const registerStartedAt = Date.now();
2755
+ await registerChildRunWithRuntime({
2647
2756
  env: input.env,
2757
+ baseUrl,
2758
+ childExecutorToken: childToken,
2759
+ childRunId,
2760
+ childPlayName,
2761
+ manifest,
2762
+ governance,
2763
+ runtimeBackend: 'cf_workflows_dynamic_worker',
2764
+ schedulerBackend: 'cf_workflows',
2765
+ executionProfile: 'workers_edge',
2648
2766
  });
2767
+ trace(
2768
+ 'coordinator.child_submit_register_run',
2769
+ registerStartedAt,
2770
+ manifest.graphHash,
2771
+ {
2772
+ childRunId,
2773
+ childPlayName,
2774
+ },
2775
+ );
2776
+
2777
+ const workflowSubmitStartedAt = Date.now();
2778
+ let response: Response;
2779
+ try {
2780
+ response = await handleWorkflowRoute({
2781
+ runId: childRunId,
2782
+ action: 'submit',
2783
+ request: new Request(
2784
+ `https://deepline.coordinator.internal/workflow/${encodeURIComponent(
2785
+ childRunId,
2786
+ )}/submit`,
2787
+ {
2788
+ method: 'POST',
2789
+ headers: { 'content-type': 'application/json' },
2790
+ body: JSON.stringify(params),
2791
+ },
2792
+ ),
2793
+ env: input.env,
2794
+ });
2795
+ } catch (error) {
2796
+ await markRegisteredChildRunFailed({
2797
+ env: input.env,
2798
+ baseUrl,
2799
+ childExecutorToken: childToken,
2800
+ childRunId,
2801
+ error,
2802
+ }).catch((markError) => {
2803
+ console.error('[coordinator] child workflow submit failure mark failed', {
2804
+ childRunId,
2805
+ error:
2806
+ markError instanceof Error ? markError.message : String(markError),
2807
+ });
2808
+ });
2809
+ throw error;
2810
+ }
2649
2811
  trace(
2650
2812
  'coordinator.child_submit_workflow',
2651
2813
  workflowSubmitStartedAt,
@@ -2653,6 +2815,21 @@ async function submitChildWorkflowThroughCoordinator(input: {
2653
2815
  { childRunId, status: response.status },
2654
2816
  );
2655
2817
  const responseText = await response.text().catch(() => '');
2818
+ if (!response.ok) {
2819
+ await markRegisteredChildRunFailed({
2820
+ env: input.env,
2821
+ baseUrl,
2822
+ childExecutorToken: childToken,
2823
+ childRunId,
2824
+ error: `workflow submit returned ${response.status}: ${responseText.slice(0, 800)}`,
2825
+ }).catch((markError) => {
2826
+ console.error('[coordinator] child workflow submit failure mark failed', {
2827
+ childRunId,
2828
+ error:
2829
+ markError instanceof Error ? markError.message : String(markError),
2830
+ });
2831
+ });
2832
+ }
2656
2833
  return {
2657
2834
  response,
2658
2835
  responseText,
@@ -3025,6 +3202,9 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
3025
3202
  typeof metadata.dynamicWorkerCode === 'string'
3026
3203
  ? metadata.dynamicWorkerCode
3027
3204
  : null,
3205
+ integrationMode: normalizeIntegrationMode(
3206
+ metadata.integrationMode,
3207
+ ),
3028
3208
  packagedFiles: normalizePackagedFiles(metadata.packagedFiles),
3029
3209
  },
3030
3210
  trace,
@@ -3612,6 +3792,7 @@ async function handleWorkflowRoute(input: {
3612
3792
  ms: Date.now() - submitStartedAt,
3613
3793
  graphHash: params.graphHash ?? null,
3614
3794
  extra: {
3795
+ integrationMode: normalizeIntegrationMode(params.integrationMode),
3615
3796
  hasDynamicWorkerCode: Boolean(params.dynamicWorkerCode),
3616
3797
  dynamicWorkerBytes:
3617
3798
  typeof params.dynamicWorkerCode === 'string'
@@ -3624,6 +3805,7 @@ async function handleWorkflowRoute(input: {
3624
3805
  ms: Date.now() - parseStartedAt,
3625
3806
  graphHash: params.graphHash ?? null,
3626
3807
  extra: {
3808
+ integrationMode: normalizeIntegrationMode(params.integrationMode),
3627
3809
  hasDynamicWorkerCode: Boolean(params.dynamicWorkerCode),
3628
3810
  dynamicWorkerBytes:
3629
3811
  typeof params.dynamicWorkerCode === 'string'
@@ -3746,6 +3928,92 @@ async function handleWorkflowRoute(input: {
3746
3928
  workflowParams.submittedAt = Date.now();
3747
3929
  let instance: WorkflowInstance | null = null;
3748
3930
  try {
3931
+ const submitClaimStartedAt = Date.now();
3932
+ const submitClaim = await claimWorkflowSubmit({
3933
+ env,
3934
+ runId: submittedRunId,
3935
+ });
3936
+ recordSubmitTiming({
3937
+ phase: 'coordinator.workflow_submit_claim',
3938
+ ms: Date.now() - submitClaimStartedAt,
3939
+ graphHash: params.graphHash ?? null,
3940
+ extra: {
3941
+ claimed: submitClaim.claimed,
3942
+ status: submitClaim.status,
3943
+ instanceId: submitClaim.instanceId ?? defaultInstanceId,
3944
+ },
3945
+ });
3946
+ if (!submitClaim.claimed) {
3947
+ const duplicateWaitStartedAt = Date.now();
3948
+ const resolvedSubmitClaim = await waitForWorkflowSubmitCreated({
3949
+ env,
3950
+ runId: submittedRunId,
3951
+ initialClaim: submitClaim,
3952
+ timeoutMs: WORKFLOW_SUBMIT_DUPLICATE_WAIT_MS,
3953
+ pollMs: WORKFLOW_SUBMIT_DUPLICATE_POLL_MS,
3954
+ });
3955
+ recordSubmitTiming({
3956
+ phase: 'coordinator.workflow_submit_duplicate_wait',
3957
+ ms: Date.now() - duplicateWaitStartedAt,
3958
+ graphHash: params.graphHash ?? null,
3959
+ extra: {
3960
+ status: resolvedSubmitClaim.status,
3961
+ instanceId: resolvedSubmitClaim.instanceId ?? null,
3962
+ },
3963
+ });
3964
+ const totalMs = Date.now() - submitStartedAt;
3965
+ if (
3966
+ resolvedSubmitClaim.status !== 'created' ||
3967
+ !resolvedSubmitClaim.instanceId
3968
+ ) {
3969
+ recordSubmitTiming({
3970
+ phase: 'coordinator.submit_total',
3971
+ ms: totalMs,
3972
+ graphHash: params.graphHash ?? null,
3973
+ extra: { duplicate: true, pending: true },
3974
+ });
3975
+ return Response.json(
3976
+ {
3977
+ runId,
3978
+ status: 'pending',
3979
+ workflowInstanceId: null,
3980
+ instanceState: {
3981
+ status: resolvedSubmitClaim.status ?? 'creating',
3982
+ },
3983
+ retryAfterMs: 250,
3984
+ runtimeDeployVersion:
3985
+ env.CF_VERSION_METADATA?.id ??
3986
+ env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ??
3987
+ null,
3988
+ coordinatorTimings,
3989
+ },
3990
+ { status: 503, headers: { 'retry-after': '1' } },
3991
+ );
3992
+ }
3993
+ recordSubmitTiming({
3994
+ phase: 'coordinator.submit_total',
3995
+ ms: totalMs,
3996
+ graphHash: params.graphHash ?? null,
3997
+ extra: { duplicate: true },
3998
+ });
3999
+ recordSubmitTiming({
4000
+ phase: 'coordinator.submit_accepted',
4001
+ ms: totalMs,
4002
+ graphHash: params.graphHash ?? null,
4003
+ extra: { duplicate: true },
4004
+ });
4005
+ return Response.json({
4006
+ runId,
4007
+ status: 'submitted',
4008
+ workflowInstanceId: resolvedSubmitClaim.instanceId,
4009
+ instanceState: { status: resolvedSubmitClaim.status },
4010
+ runtimeDeployVersion:
4011
+ env.CF_VERSION_METADATA?.id ??
4012
+ env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ??
4013
+ null,
4014
+ coordinatorTimings,
4015
+ });
4016
+ }
3749
4017
  const dispatchStartedAt = Date.now();
3750
4018
  const createStartedAt = Date.now();
3751
4019
  recordSubmitTiming({
@@ -3762,6 +4030,12 @@ async function handleWorkflowRoute(input: {
3762
4030
  params: workflowParams,
3763
4031
  }),
3764
4032
  getExisting: () => env.PLAY_WORKFLOW.get(defaultInstanceId),
4033
+ onCreateFailure: async () => {
4034
+ await releaseWorkflowSubmitClaim({
4035
+ env,
4036
+ runId: submittedRunId,
4037
+ });
4038
+ },
3765
4039
  });
3766
4040
  instance = createResult.instance;
3767
4041
  const workflowCreatedAt = Date.now();
@@ -3771,18 +4045,35 @@ async function handleWorkflowRoute(input: {
3771
4045
  graphHash: params.graphHash ?? null,
3772
4046
  extra: { instanceId: instance.id, startMode: createResult.startMode },
3773
4047
  });
3774
- const instanceIdRecord = recordWorkflowInstanceId({
3775
- env,
3776
- runId: submittedRunId,
3777
- instanceId: instance.id,
3778
- }).catch((error) => {
4048
+ try {
4049
+ await recordWorkflowInstanceId({
4050
+ env,
4051
+ runId: submittedRunId,
4052
+ instanceId: instance.id,
4053
+ });
4054
+ } catch (error) {
3779
4055
  console.warn('[coordinator] workflow instance id record failed', {
3780
4056
  runId: submittedRunId,
3781
4057
  instanceId: instance?.id ?? null,
3782
4058
  error: error instanceof Error ? error.message : String(error),
3783
4059
  });
3784
- });
3785
- input.ctx?.waitUntil(instanceIdRecord);
4060
+ await releaseWorkflowSubmitClaim({
4061
+ env,
4062
+ runId: submittedRunId,
4063
+ }).catch((releaseError) => {
4064
+ console.warn(
4065
+ '[coordinator] workflow submit claim release after instance record failure failed',
4066
+ {
4067
+ runId: submittedRunId,
4068
+ error:
4069
+ releaseError instanceof Error
4070
+ ? releaseError.message
4071
+ : String(releaseError),
4072
+ },
4073
+ );
4074
+ });
4075
+ throw error;
4076
+ }
3786
4077
  recordSubmitTiming({
3787
4078
  phase: 'coordinator.dispatch_workflow',
3788
4079
  ms: Date.now() - dispatchStartedAt,
@@ -3790,7 +4081,7 @@ async function handleWorkflowRoute(input: {
3790
4081
  extra: {
3791
4082
  startMode: 'direct_workflow_create',
3792
4083
  workflowCreateMode: createResult.startMode,
3793
- instanceIdRecord: 'waitUntil',
4084
+ instanceIdRecord: 'recorded',
3794
4085
  },
3795
4086
  });
3796
4087
  const initialWaitMsRaw = Number(
@@ -102,6 +102,15 @@ type WorkflowInstanceState = {
102
102
  expiresAt: number;
103
103
  };
104
104
 
105
+ type WorkflowSubmitClaimState = {
106
+ runId: string;
107
+ status: 'creating' | 'created';
108
+ instanceId?: string;
109
+ claimedAt: number;
110
+ updatedAt: number;
111
+ expiresAt: number;
112
+ };
113
+
105
114
  type WorkflowDbSessionsState = {
106
115
  runId: string;
107
116
  sessions: PreloadedRuntimeDbSession[];
@@ -357,6 +366,10 @@ export class PlayDedup implements DurableObject {
357
366
  return await this.handleWorkflowInstancePut(req);
358
367
  case '/workflow-instance-get':
359
368
  return await this.handleWorkflowInstanceGet(req);
369
+ case '/workflow-submit-claim':
370
+ return await this.handleWorkflowSubmitClaim(req);
371
+ case '/workflow-submit-release':
372
+ return await this.handleWorkflowSubmitRelease(req);
360
373
  case '/run-retry-claim':
361
374
  return await this.handleRunRetryClaim(req);
362
375
  case '/db-sessions-put':
@@ -787,12 +800,22 @@ export class PlayDedup implements DurableObject {
787
800
  body.ttlMs > 0
788
801
  ? Math.max(60_000, Math.min(body.ttlMs, WORKFLOW_RUN_STATE_TTL_MS))
789
802
  : WORKFLOW_RUN_STATE_TTL_MS;
790
- await this.state.storage.put('workflow-instance', {
791
- runId,
792
- instanceId,
793
- updatedAt: now,
794
- expiresAt: now + ttlMs,
795
- } satisfies WorkflowInstanceState);
803
+ await this.state.storage.put({
804
+ 'workflow-instance': {
805
+ runId,
806
+ instanceId,
807
+ updatedAt: now,
808
+ expiresAt: now + ttlMs,
809
+ } satisfies WorkflowInstanceState,
810
+ 'workflow-submit-claim': {
811
+ runId,
812
+ status: 'created',
813
+ instanceId,
814
+ claimedAt: now,
815
+ updatedAt: now,
816
+ expiresAt: now + ttlMs,
817
+ } satisfies WorkflowSubmitClaimState,
818
+ });
796
819
  return new Response(JSON.stringify({ ok: true }), {
797
820
  headers: { 'content-type': 'application/json' },
798
821
  });
@@ -821,6 +844,76 @@ export class PlayDedup implements DurableObject {
821
844
  });
822
845
  }
823
846
 
847
+ private async handleWorkflowSubmitClaim(req: Request): Promise<Response> {
848
+ const body = (await req.json().catch(() => null)) as {
849
+ runId?: unknown;
850
+ ttlMs?: unknown;
851
+ } | null;
852
+ const runId = typeof body?.runId === 'string' ? body.runId : '';
853
+ if (!runId) {
854
+ return new Response('runId is required', { status: 400 });
855
+ }
856
+ const now = Date.now();
857
+ const ttlMs =
858
+ typeof body?.ttlMs === 'number' &&
859
+ Number.isFinite(body.ttlMs) &&
860
+ body.ttlMs > 0
861
+ ? Math.max(60_000, Math.min(body.ttlMs, WORKFLOW_RUN_STATE_TTL_MS))
862
+ : WORKFLOW_RUN_STATE_TTL_MS;
863
+ let response: {
864
+ claimed: boolean;
865
+ status: WorkflowSubmitClaimState['status'];
866
+ instanceId: string | null;
867
+ } = { claimed: true, status: 'creating', instanceId: null };
868
+ await this.state.blockConcurrencyWhile(async () => {
869
+ const key = 'workflow-submit-claim';
870
+ const existing =
871
+ await this.state.storage.get<WorkflowSubmitClaimState>(key);
872
+ if (existing?.runId === runId && existing.expiresAt > now) {
873
+ response = {
874
+ claimed: false,
875
+ status: existing.status,
876
+ instanceId: existing.instanceId ?? null,
877
+ };
878
+ return;
879
+ }
880
+ const state = {
881
+ runId,
882
+ status: 'creating',
883
+ claimedAt: now,
884
+ updatedAt: now,
885
+ expiresAt: now + ttlMs,
886
+ } satisfies WorkflowSubmitClaimState;
887
+ await this.state.storage.put(key, state);
888
+ });
889
+ return new Response(JSON.stringify(response), {
890
+ headers: { 'content-type': 'application/json' },
891
+ });
892
+ }
893
+
894
+ private async handleWorkflowSubmitRelease(req: Request): Promise<Response> {
895
+ const body = (await req.json().catch(() => null)) as {
896
+ runId?: unknown;
897
+ } | null;
898
+ const runId = typeof body?.runId === 'string' ? body.runId : '';
899
+ if (!runId) {
900
+ return new Response('runId is required', { status: 400 });
901
+ }
902
+ let released = false;
903
+ await this.state.blockConcurrencyWhile(async () => {
904
+ const key = 'workflow-submit-claim';
905
+ const existing =
906
+ await this.state.storage.get<WorkflowSubmitClaimState>(key);
907
+ if (existing?.runId === runId && existing.status === 'creating') {
908
+ await this.state.storage.delete(key);
909
+ released = true;
910
+ }
911
+ });
912
+ return new Response(JSON.stringify({ ok: true, released }), {
913
+ headers: { 'content-type': 'application/json' },
914
+ });
915
+ }
916
+
824
917
  private async handleDbSessionsPut(req: Request): Promise<Response> {
825
918
  const body = (await req.json().catch(() => null)) as {
826
919
  runId?: unknown;