deepline 0.1.19 → 0.1.21

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.
@@ -23,6 +23,7 @@ import {
23
23
  } from '@cloudflare/dynamic-workflows';
24
24
  import type { ExecutionPlan } from '../../../shared_libs/play-runtime/execution-plan';
25
25
  import type { PlayCallGovernanceSnapshot } from '../../../shared_libs/play-runtime/scheduler-backend';
26
+ import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
26
27
  import type {
27
28
  PlayRuntimeManifest,
28
29
  PlayRuntimeManifestMap,
@@ -54,6 +55,7 @@ export type PlayWorkflowParams = {
54
55
  executionPlan?: ExecutionPlan | null;
55
56
  childPlayManifests?: PlayRuntimeManifestMap | null;
56
57
  playCallGovernance?: PlayCallGovernanceSnapshot | null;
58
+ preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
57
59
  dynamicWorkerCode?: string | null;
58
60
  executorToken: string;
59
61
  baseUrl: string;
@@ -115,6 +117,57 @@ type CoordinatorPerfTraceInput = {
115
117
 
116
118
  type CoordinatorPerfTraceSink = (event: CoordinatorPerfTraceInput) => void;
117
119
 
120
+ type CoordinatorTerminalState = {
121
+ runId: string;
122
+ status: 'completed' | 'failed' | 'cancelled';
123
+ result?: unknown;
124
+ error?: string | null;
125
+ totalRows?: unknown;
126
+ durationMs?: unknown;
127
+ playName?: string | null;
128
+ completedAt?: number;
129
+ };
130
+
131
+ type CoordinatorRunEvent =
132
+ | {
133
+ seq?: number;
134
+ runId: string;
135
+ type: 'status';
136
+ status: string;
137
+ ts: number;
138
+ logs?: string[];
139
+ }
140
+ | {
141
+ seq?: number;
142
+ runId: string;
143
+ type: 'log';
144
+ line: string;
145
+ ts: number;
146
+ }
147
+ | {
148
+ seq?: number;
149
+ runId: string;
150
+ type: 'progress';
151
+ status: string;
152
+ ts: number;
153
+ logs?: string[];
154
+ activeNodeId?: string | null;
155
+ activeArtifactTableNamespace?: string | null;
156
+ updatedAt?: number | null;
157
+ }
158
+ | {
159
+ seq?: number;
160
+ runId: string;
161
+ type: 'terminal';
162
+ status: 'completed' | 'failed' | 'cancelled';
163
+ ts: number;
164
+ result?: unknown;
165
+ error?: string | null;
166
+ totalRows?: unknown;
167
+ durationMs?: unknown;
168
+ playName?: string | null;
169
+ };
170
+
118
171
  type InlineWorkerRunResponse = {
119
172
  status?: 'completed' | 'failed';
120
173
  result?: unknown;
@@ -156,6 +209,7 @@ interface CoordinatorEnv {
156
209
  DEEPLINE_API_BASE_URL: string;
157
210
  DEEPLINE_INTERNAL_TOKEN?: string;
158
211
  DEEPLINE_TAIL_LOG_TOKEN?: string;
212
+ DEEPLINE_COORDINATOR_DEPLOY_MARKER?: string;
159
213
  VERCEL_PROTECTION_BYPASS_TOKEN?: string;
160
214
  DEEPLINE_PLAY_PREVIEW_SLUG?: string;
161
215
  /**
@@ -175,7 +229,12 @@ interface CoordinatorEnv {
175
229
  HARNESS?: import('../../play-harness-worker/src/rpc-types').PlayHarnessRpc;
176
230
  }
177
231
 
178
- const WORKFLOW_READ_ONLY_ACTIONS = new Set(['', 'observe', 'result', 'status']);
232
+ const WORKFLOW_READ_ONLY_ACTIONS = new Set([
233
+ '',
234
+ 'result',
235
+ 'status',
236
+ 'tail',
237
+ ]);
179
238
 
180
239
  function authorizeCoordinatorControlRequest(input: {
181
240
  request: Request;
@@ -326,6 +385,106 @@ async function listCoordinatorPerfTrace(
326
385
  );
327
386
  }
328
387
 
388
+ async function writeCoordinatorTerminalState(
389
+ env: CoordinatorEnv,
390
+ state: CoordinatorTerminalState,
391
+ ): Promise<void> {
392
+ const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(state.runId));
393
+ const response = await stub.fetch(
394
+ 'https://deepline.dedup.internal/terminal-set',
395
+ {
396
+ method: 'POST',
397
+ headers: { 'content-type': 'application/json' },
398
+ body: JSON.stringify({
399
+ ...state,
400
+ completedAt: state.completedAt ?? Date.now(),
401
+ }),
402
+ },
403
+ );
404
+ if (!response.ok) {
405
+ throw new Error(`coordinator terminal set failed ${response.status}`);
406
+ }
407
+ }
408
+
409
+ async function appendCoordinatorRunEvent(
410
+ env: CoordinatorEnv,
411
+ event: CoordinatorRunEvent,
412
+ ): Promise<void> {
413
+ const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(event.runId));
414
+ const response = await stub.fetch('https://deepline.dedup.internal/event-add', {
415
+ method: 'POST',
416
+ headers: { 'content-type': 'application/json' },
417
+ body: JSON.stringify(event),
418
+ });
419
+ if (!response.ok) {
420
+ throw new Error(`coordinator event append failed ${response.status}`);
421
+ }
422
+ }
423
+
424
+ async function listCoordinatorRunEvents(input: {
425
+ env: CoordinatorEnv;
426
+ runId: string;
427
+ afterSeq: number;
428
+ timeoutMs: number;
429
+ }): Promise<{ events: CoordinatorRunEvent[]; latestSeq: number }> {
430
+ const stub = input.env.PLAY_DEDUP.get(
431
+ input.env.PLAY_DEDUP.idFromName(input.runId),
432
+ );
433
+ const response = await stub.fetch(
434
+ `https://deepline.dedup.internal/event-list?afterSeq=${encodeURIComponent(
435
+ String(Math.max(0, Math.floor(input.afterSeq))),
436
+ )}&timeoutMs=${encodeURIComponent(
437
+ String(Math.max(0, Math.floor(input.timeoutMs))),
438
+ )}`,
439
+ );
440
+ if (!response.ok) {
441
+ throw new Error(`coordinator event list failed ${response.status}`);
442
+ }
443
+ const body = (await response.json().catch(() => ({}))) as {
444
+ events?: unknown;
445
+ latestSeq?: unknown;
446
+ };
447
+ return {
448
+ events: Array.isArray(body.events)
449
+ ? body.events.filter(
450
+ (event): event is CoordinatorRunEvent =>
451
+ isRecord(event) &&
452
+ typeof event.runId === 'string' &&
453
+ event.runId === input.runId &&
454
+ typeof event.type === 'string' &&
455
+ typeof event.ts === 'number',
456
+ )
457
+ : [],
458
+ latestSeq: typeof body.latestSeq === 'number' ? body.latestSeq : input.afterSeq,
459
+ };
460
+ }
461
+
462
+ async function readCoordinatorTerminalState(
463
+ env: CoordinatorEnv,
464
+ runId: string,
465
+ ): Promise<CoordinatorTerminalState | null> {
466
+ const stub = env.PLAY_DEDUP.get(env.PLAY_DEDUP.idFromName(runId));
467
+ const response = await stub.fetch(
468
+ 'https://deepline.dedup.internal/terminal-get',
469
+ );
470
+ if (!response.ok) {
471
+ throw new Error(`coordinator terminal get failed ${response.status}`);
472
+ }
473
+ const body = (await response.json().catch(() => ({}))) as {
474
+ state?: unknown;
475
+ };
476
+ const state = body.state;
477
+ if (!isRecord(state) || state.runId !== runId) return null;
478
+ if (
479
+ state.status !== 'completed' &&
480
+ state.status !== 'failed' &&
481
+ state.status !== 'cancelled'
482
+ ) {
483
+ return null;
484
+ }
485
+ return state as CoordinatorTerminalState;
486
+ }
487
+
329
488
  function workflowEventType(name: string): string {
330
489
  const normalized = name
331
490
  .trim()
@@ -360,16 +519,20 @@ type PooledWorkflowBootstrapPayload = {
360
519
  };
361
520
 
362
521
  const WORKFLOW_POOL_PROTOCOL_VERSION =
363
- 'pooled-workflow-wait-v4-waiting-only';
522
+ 'pooled-workflow-wait-v14-ready-signal-http-storage';
364
523
  const WORKFLOW_POOL_DO_NAME = 'workflow-pool:v2';
365
524
  const WORKFLOW_POOL_START_EVENT_TYPE = 'play_start';
366
525
  const WORKFLOW_POOL_TTL_MS = 8 * 60 * 1000;
367
- const WORKFLOW_POOL_TARGET_SIZE = 2;
526
+ const WORKFLOW_POOL_TARGET_SIZE = 16;
368
527
  const WORKFLOW_POOL_READY_TIMEOUT_MS = 1_500;
369
528
  const WORKFLOW_POOL_READY_POLL_MS = 250;
370
529
  const WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS = 2_500;
371
- const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 1;
372
-
530
+ const WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE = 4;
531
+ const WORKFLOW_POOL_CONTROL_TIMEOUT_MS = 750;
532
+ const SUBMIT_INITIAL_STATE_MAX_WAIT_MS = 0;
533
+ const SUBMIT_INITIAL_STATE_POLL_MS = 50;
534
+ const WORKFLOW_POOL_DISABLED_REASON =
535
+ 'Cloudflare Workflows start runs directly; waitForEvent is reserved for real durable external waits.';
373
536
  function buildDynamicWorkflowMetadata(
374
537
  params: PlayWorkflowParams,
375
538
  ): DynamicWorkflowMetadata {
@@ -440,13 +603,45 @@ function readWorkflowTraceContext(event: unknown): {
440
603
  }
441
604
 
442
605
  function workflowPoolEnabled(): boolean {
443
- return true;
606
+ return false;
444
607
  }
445
608
 
446
609
  function workflowPoolTargetSize(): number {
447
610
  return WORKFLOW_POOL_TARGET_SIZE;
448
611
  }
449
612
 
613
+ async function waitForSubmitInitialState(input: {
614
+ instance: WorkflowInstance;
615
+ runId: string;
616
+ waitMs: number;
617
+ }): Promise<Record<string, unknown> | null> {
618
+ const waitMs = Math.max(
619
+ 0,
620
+ Math.min(Math.floor(input.waitMs), SUBMIT_INITIAL_STATE_MAX_WAIT_MS),
621
+ );
622
+ if (waitMs <= 0) return null;
623
+ const startedAt = Date.now();
624
+ let status = await input.instance.status();
625
+ while (Date.now() - startedAt < waitMs) {
626
+ const result = mapWorkflowResult(input.runId, status);
627
+ if (
628
+ result.status === 'completed' ||
629
+ result.status === 'failed' ||
630
+ result.status === 'cancelled'
631
+ ) {
632
+ return result as unknown as Record<string, unknown>;
633
+ }
634
+ await sleep(SUBMIT_INITIAL_STATE_POLL_MS);
635
+ status = await input.instance.status();
636
+ }
637
+ const result = mapWorkflowResult(input.runId, status);
638
+ return result.status === 'completed' ||
639
+ result.status === 'failed' ||
640
+ result.status === 'cancelled'
641
+ ? (result as unknown as Record<string, unknown>)
642
+ : null;
643
+ }
644
+
450
645
  async function createDynamicWorkflowInstance(input: {
451
646
  env: CoordinatorEnv;
452
647
  id: string;
@@ -469,26 +664,57 @@ function workflowPoolDurableObject(env: CoordinatorEnv): DurableObjectStub {
469
664
  async function callWorkflowPool<T>(
470
665
  env: CoordinatorEnv,
471
666
  path: string,
472
- init?: RequestInit,
667
+ init?: RequestInit & { timeoutMs?: number },
473
668
  ): Promise<T> {
474
- const response = await workflowPoolDurableObject(env).fetch(
475
- `https://deepline.workflow-pool.internal${path}`,
476
- {
477
- ...init,
478
- headers: {
479
- 'content-type': 'application/json',
480
- ...(init?.headers ?? {}),
481
- },
482
- },
669
+ const timeoutMs = Math.max(
670
+ 1,
671
+ Math.floor(init?.timeoutMs ?? WORKFLOW_POOL_CONTROL_TIMEOUT_MS),
483
672
  );
484
- if (!response.ok) {
485
- throw new Error(
486
- `workflow pool ${path} failed ${response.status}: ${(
487
- await response.text().catch(() => '')
488
- ).slice(0, 400)}`,
489
- );
673
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
674
+ try {
675
+ const fetchInit: RequestInit = { ...(init ?? {}) };
676
+ delete (fetchInit as { timeoutMs?: number }).timeoutMs;
677
+ delete fetchInit.signal;
678
+ const response = await Promise.race([
679
+ workflowPoolDurableObject(env).fetch(
680
+ `https://deepline.workflow-pool.internal${path}`,
681
+ {
682
+ ...fetchInit,
683
+ headers: {
684
+ 'content-type': 'application/json',
685
+ ...(init?.headers ?? {}),
686
+ },
687
+ },
688
+ ),
689
+ new Promise<Response>((_, reject) => {
690
+ timeoutId = setTimeout(
691
+ () =>
692
+ reject(
693
+ new Error(`workflow pool ${path} timed out after ${timeoutMs}ms`),
694
+ ),
695
+ timeoutMs,
696
+ );
697
+ }),
698
+ ]);
699
+ if (!response.ok) {
700
+ throw new Error(
701
+ `workflow pool ${path} failed ${response.status}: ${(
702
+ await response.text().catch(() => '')
703
+ ).slice(0, 400)}`,
704
+ );
705
+ }
706
+ return (await response.json()) as T;
707
+ } catch (error) {
708
+ if (
709
+ error instanceof Error &&
710
+ (error.name === 'AbortError' || error.message.includes('aborted'))
711
+ ) {
712
+ throw new Error(`workflow pool ${path} timed out after ${timeoutMs}ms`);
713
+ }
714
+ throw error;
715
+ } finally {
716
+ if (timeoutId) clearTimeout(timeoutId);
490
717
  }
491
- return (await response.json()) as T;
492
718
  }
493
719
 
494
720
  type WorkflowPoolCounts = {
@@ -507,6 +733,7 @@ type WorkflowPoolRefillResult = WorkflowPoolCounts & {
507
733
 
508
734
  type WorkflowPoolListEntry = {
509
735
  id: string;
736
+ state: string;
510
737
  createdAt: number;
511
738
  readyAt: number | null;
512
739
  expiresAt: number;
@@ -544,6 +771,7 @@ async function listWorkflowPoolEntries(
544
771
  )
545
772
  .map((entry) => ({
546
773
  id: typeof entry.id === 'string' ? entry.id : '',
774
+ state: typeof entry.state === 'string' ? entry.state : '',
547
775
  createdAt:
548
776
  typeof entry.createdAt === 'number' && Number.isFinite(entry.createdAt)
549
777
  ? entry.createdAt
@@ -578,6 +806,24 @@ async function addWorkflowPoolIds(
578
806
  });
579
807
  }
580
808
 
809
+ async function markWorkflowPoolIdReady(
810
+ env: CoordinatorEnv,
811
+ poolId: string,
812
+ ): Promise<boolean> {
813
+ const body = await callWorkflowPool<{ ready?: unknown }>(
814
+ env,
815
+ '/pool-ready',
816
+ {
817
+ method: 'POST',
818
+ body: JSON.stringify({
819
+ poolId,
820
+ version: WORKFLOW_POOL_PROTOCOL_VERSION,
821
+ }),
822
+ },
823
+ );
824
+ return body.ready === true;
825
+ }
826
+
581
827
  async function promoteWorkflowPoolIds(
582
828
  env: CoordinatorEnv,
583
829
  ids: string[],
@@ -608,13 +854,14 @@ async function deleteWorkflowPoolIds(
608
854
 
609
855
  async function leaseWorkflowPoolId(
610
856
  env: CoordinatorEnv,
857
+ runId: string,
611
858
  ): Promise<string | null> {
612
859
  const body = await callWorkflowPool<{ id?: unknown }>(
613
860
  env,
614
- `/pool-lease?version=${encodeURIComponent(WORKFLOW_POOL_PROTOCOL_VERSION)}`,
861
+ `/pool-claim?version=${encodeURIComponent(WORKFLOW_POOL_PROTOCOL_VERSION)}`,
615
862
  {
616
863
  method: 'POST',
617
- body: '{}',
864
+ body: JSON.stringify({ runId }),
618
865
  },
619
866
  );
620
867
  return typeof body.id === 'string' && body.id ? body.id : null;
@@ -678,27 +925,36 @@ function workflowStatusName(status: InstanceStatus | null): string {
678
925
  }
679
926
 
680
927
  function workflowPoolStatusIsReady(statusName: string): boolean {
681
- // Only a Workflow explicitly blocked on waitForEvent is safe to lease.
682
- // A generic "running" instance may already be executing a previous play; if
683
- // we send a new start event to it, the submitted run can stay running forever
684
- // while callbacks continue updating the previous runId.
685
- return statusName === 'waiting';
928
+ // This is only a liveness guard. Readiness itself comes from the pooled
929
+ // Workflow calling /pool-ready after waitForEvent("play_start") has been
930
+ // created, because Cloudflare may report an armed wait as "running".
931
+ return statusName === 'running' || statusName === 'waiting';
686
932
  }
687
933
 
688
- async function waitForWorkflowPoolReady(instance: WorkflowInstance): Promise<{
934
+ async function waitForWorkflowPoolReadySignal(input: {
935
+ env: CoordinatorEnv;
936
+ instance: WorkflowInstance;
937
+ poolId: string;
938
+ }): Promise<{
689
939
  ready: boolean;
690
940
  status: string;
691
941
  ms: number;
692
942
  polls: number;
693
943
  }> {
694
944
  const startedAt = Date.now();
695
- let lastStatus: InstanceStatus | null = null;
945
+ let lastStatusName = 'unknown';
696
946
  let polls = 0;
697
947
  while (Date.now() - startedAt < WORKFLOW_POOL_READY_TIMEOUT_MS) {
698
- lastStatus = await instance.status();
699
948
  polls += 1;
700
- const statusName = workflowStatusName(lastStatus);
701
- if (workflowPoolStatusIsReady(statusName)) {
949
+ const [entry, status] = await Promise.all([
950
+ listWorkflowPoolEntries(input.env)
951
+ .then((entries) => entries.find((candidate) => candidate.id === input.poolId))
952
+ .catch(() => undefined),
953
+ input.instance.status().catch(() => null),
954
+ ]);
955
+ const statusName = workflowStatusName(status);
956
+ lastStatusName = statusName;
957
+ if (entry?.state === 'ready' && entry.readyAt !== null) {
702
958
  return {
703
959
  ready: true,
704
960
  status: statusName,
@@ -709,7 +965,8 @@ async function waitForWorkflowPoolReady(instance: WorkflowInstance): Promise<{
709
965
  if (
710
966
  statusName === 'complete' ||
711
967
  statusName === 'errored' ||
712
- statusName === 'terminated'
968
+ statusName === 'terminated' ||
969
+ statusName === 'unknown'
713
970
  ) {
714
971
  return {
715
972
  ready: false,
@@ -718,13 +975,11 @@ async function waitForWorkflowPoolReady(instance: WorkflowInstance): Promise<{
718
975
  polls,
719
976
  };
720
977
  }
721
- await new Promise((resolve) =>
722
- setTimeout(resolve, WORKFLOW_POOL_READY_POLL_MS),
723
- );
978
+ await sleep(WORKFLOW_POOL_READY_POLL_MS);
724
979
  }
725
980
  return {
726
981
  ready: false,
727
- status: workflowStatusName(lastStatus),
982
+ status: lastStatusName,
728
983
  ms: Date.now() - startedAt,
729
984
  polls,
730
985
  };
@@ -751,11 +1006,13 @@ async function refillWorkflowPoolOnce(
751
1006
  for (const entry of warmingEntries) {
752
1007
  const instance = await env.PLAY_WORKFLOW.get(entry.id);
753
1008
  try {
1009
+ if (entry.state === 'ready' && entry.readyAt !== null) {
1010
+ promotedIds.push(entry.id);
1011
+ continue;
1012
+ }
754
1013
  const status = await instance.status().catch(() => null);
755
1014
  const statusName = workflowStatusName(status);
756
- if (workflowPoolStatusIsReady(statusName)) {
757
- promotedIds.push(entry.id);
758
- } else if (
1015
+ if (
759
1016
  statusName === 'complete' ||
760
1017
  statusName === 'errored' ||
761
1018
  statusName === 'terminated' ||
@@ -784,48 +1041,64 @@ async function refillWorkflowPoolOnce(
784
1041
  removed: removedIds.length,
785
1042
  };
786
1043
  }
787
- const readyCreatedIds: string[] = [];
788
- const warmingCreatedIds: string[] = [];
789
- for (let i = 0; i < needed; i += 1) {
790
- const poolId = `pool-v2-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 12)}`;
791
- const instance = await env.PLAY_WORKFLOW.create({
792
- id: poolId,
793
- params: {
794
- __deeplinePooledWorkflow: true,
795
- poolId,
796
- createdAt: Date.now(),
797
- } satisfies PooledWorkflowBootstrapPayload,
798
- });
799
- try {
800
- const readiness = await waitForWorkflowPoolReady(instance);
801
- recordCoordinatorPerfTrace({
802
- runId: poolId,
803
- phase: 'coordinator.workflow_pool_ready',
804
- ms: readiness.ms,
805
- graphHash: 'workflow-pool',
806
- extra: {
807
- ready: readiness.ready,
808
- status: readiness.status,
809
- polls: readiness.polls,
810
- },
1044
+ const created = await Promise.all(
1045
+ Array.from({ length: needed }, async () => {
1046
+ const poolId = `pool-v2-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 12)}`;
1047
+ await addWorkflowPoolIds(env, [poolId], { ready: false });
1048
+ const instance = await env.PLAY_WORKFLOW.create({
1049
+ id: poolId,
1050
+ params: {
1051
+ __deeplinePooledWorkflow: true,
1052
+ poolId,
1053
+ createdAt: Date.now(),
1054
+ } satisfies PooledWorkflowBootstrapPayload,
811
1055
  });
812
- if (readiness.ready) {
813
- readyCreatedIds.push(poolId);
814
- } else if (
815
- readiness.status === 'complete' ||
816
- readiness.status === 'errored' ||
817
- readiness.status === 'terminated' ||
818
- readiness.status === 'unknown'
819
- ) {
820
- removedIds.push(poolId);
821
- await instance.terminate().catch(() => undefined);
822
- } else {
823
- warmingCreatedIds.push(poolId);
1056
+ try {
1057
+ const readiness = await waitForWorkflowPoolReadySignal({
1058
+ env,
1059
+ instance,
1060
+ poolId,
1061
+ });
1062
+ recordCoordinatorPerfTrace({
1063
+ runId: poolId,
1064
+ phase: 'coordinator.workflow_pool_ready',
1065
+ ms: readiness.ms,
1066
+ graphHash: 'workflow-pool',
1067
+ extra: {
1068
+ ready: readiness.ready,
1069
+ status: readiness.status,
1070
+ polls: readiness.polls,
1071
+ },
1072
+ });
1073
+ if (readiness.ready) {
1074
+ return { id: poolId, state: 'ready' as const };
1075
+ }
1076
+ if (
1077
+ readiness.status === 'complete' ||
1078
+ readiness.status === 'errored' ||
1079
+ readiness.status === 'terminated' ||
1080
+ readiness.status === 'unknown'
1081
+ ) {
1082
+ await instance.terminate().catch(() => undefined);
1083
+ return { id: poolId, state: 'removed' as const };
1084
+ }
1085
+ return { id: poolId, state: 'warming' as const };
1086
+ } finally {
1087
+ disposeRpcStub(instance);
824
1088
  }
825
- } finally {
826
- disposeRpcStub(instance);
827
- }
828
- }
1089
+ }),
1090
+ );
1091
+ const readyCreatedIds = created
1092
+ .filter((entry) => entry.state === 'ready')
1093
+ .map((entry) => entry.id);
1094
+ const warmingCreatedIds = created
1095
+ .filter((entry) => entry.state === 'warming')
1096
+ .map((entry) => entry.id);
1097
+ removedIds.push(
1098
+ ...created
1099
+ .filter((entry) => entry.state === 'removed')
1100
+ .map((entry) => entry.id),
1101
+ );
829
1102
  await Promise.all([
830
1103
  addWorkflowPoolIds(env, readyCreatedIds, { ready: true }),
831
1104
  addWorkflowPoolIds(env, warmingCreatedIds, { ready: false }),
@@ -903,7 +1176,14 @@ async function submitViaPooledWorkflow(input: {
903
1176
  return null;
904
1177
  }
905
1178
  const leaseStartedAt = Date.now();
906
- let pooledInstanceId = await leaseWorkflowPoolId(input.env);
1179
+ let leaseError: string | null = null;
1180
+ const pooledInstanceId = await leaseWorkflowPoolId(
1181
+ input.env,
1182
+ input.params.runId,
1183
+ ).catch((error) => {
1184
+ leaseError = error instanceof Error ? error.message : String(error);
1185
+ return null;
1186
+ });
907
1187
  const missCounts = pooledInstanceId
908
1188
  ? null
909
1189
  : await workflowPoolCount(input.env).catch(() => null);
@@ -913,6 +1193,7 @@ async function submitViaPooledWorkflow(input: {
913
1193
  graphHash: input.params.graphHash ?? null,
914
1194
  extra: {
915
1195
  pooled: Boolean(pooledInstanceId),
1196
+ ...(leaseError ? { error: leaseError } : {}),
916
1197
  ...(missCounts
917
1198
  ? {
918
1199
  availableAfterMiss: missCounts.available,
@@ -923,43 +1204,26 @@ async function submitViaPooledWorkflow(input: {
923
1204
  });
924
1205
 
925
1206
  if (!pooledInstanceId) {
926
- // A pool miss is often a timing gap rather than a true lack of warm
927
- // capacity. Wait briefly for refill/promotion, then retry lease once
928
- // before falling back to a cold workflow create.
929
- const refillStartedAt = Date.now();
930
- const refillResult = await refillWorkflowPool(input.env, {
931
- waitReady: true,
932
- minAvailable: WORKFLOW_POOL_REFILL_ON_MISS_MIN_AVAILABLE,
933
- waitTimeoutMs: WORKFLOW_POOL_REFILL_ON_MISS_TIMEOUT_MS,
934
- }).catch(() => null);
1207
+ // A pool miss must not block the user path. Refilling is handled by the
1208
+ // caller's waitUntil after submit, so fall through to cold create now.
1209
+ const counts = missCounts ?? (await workflowPoolCount(input.env).catch(() => null));
935
1210
  input.recordSubmitTiming({
936
1211
  phase: 'coordinator.workflow_pool_refill_on_miss',
937
- ms: Date.now() - refillStartedAt,
1212
+ ms: 0,
938
1213
  graphHash: input.params.graphHash ?? null,
939
- extra:
940
- refillResult === null
941
- ? { ok: false }
942
- : {
943
- ok: true,
944
- available: refillResult.available,
945
- warming: refillResult.warming,
946
- created: refillResult.created,
947
- promoted: refillResult.promoted,
948
- removed: refillResult.removed,
949
- waitedMs: refillResult.waitedMs,
950
- waitIterations: refillResult.waitIterations,
951
- },
1214
+ extra: {
1215
+ skipped: true,
1216
+ reason: 'pool_miss_does_not_block_submit',
1217
+ ...(counts
1218
+ ? {
1219
+ available: counts.available,
1220
+ warming: counts.warming,
1221
+ waitedMs: 0,
1222
+ waitIterations: 0,
1223
+ }
1224
+ : {}),
1225
+ },
952
1226
  });
953
- if (refillResult?.available) {
954
- const retryStartedAt = Date.now();
955
- pooledInstanceId = await leaseWorkflowPoolId(input.env);
956
- input.recordSubmitTiming({
957
- phase: 'coordinator.workflow_pool_lease_retry',
958
- ms: Date.now() - retryStartedAt,
959
- graphHash: input.params.graphHash ?? null,
960
- extra: { pooled: Boolean(pooledInstanceId) },
961
- });
962
- }
963
1227
  }
964
1228
 
965
1229
  if (!pooledInstanceId) {
@@ -967,38 +1231,26 @@ async function submitViaPooledWorkflow(input: {
967
1231
  }
968
1232
 
969
1233
  const instance = await input.env.PLAY_WORKFLOW.get(pooledInstanceId);
1234
+ const readyCheckStartedAt = Date.now();
1235
+ const status = await instance.status().catch(() => null);
1236
+ const statusName = workflowStatusName(status);
1237
+ input.recordSubmitTiming({
1238
+ phase: 'coordinator.workflow_pool_ready_check',
1239
+ ms: Date.now() - readyCheckStartedAt,
1240
+ graphHash: input.params.graphHash ?? null,
1241
+ extra: { instanceId: pooledInstanceId, status: statusName },
1242
+ });
1243
+ if (!workflowPoolStatusIsReady(statusName)) {
1244
+ await instance.terminate().catch(() => undefined);
1245
+ disposeRpcStub(instance);
1246
+ return null;
1247
+ }
1248
+ const sendStartedAt = Date.now();
970
1249
  try {
971
- const readyCheckStartedAt = Date.now();
972
- const status = await instance.status().catch(() => null);
973
- const statusName = workflowStatusName(status);
974
- input.recordSubmitTiming({
975
- phase: 'coordinator.workflow_pool_ready_check',
976
- ms: Date.now() - readyCheckStartedAt,
977
- graphHash: input.params.graphHash ?? null,
978
- extra: { instanceId: pooledInstanceId, status: statusName },
979
- });
980
- if (!workflowPoolStatusIsReady(statusName)) {
981
- await instance.terminate().catch(() => undefined);
982
- disposeRpcStub(instance);
983
- return null;
984
- }
985
- const sendStartedAt = Date.now();
986
1250
  await instance.sendEvent({
987
1251
  type: WORKFLOW_POOL_START_EVENT_TYPE,
988
1252
  payload: buildDispatcherEnvelope(input.params),
989
1253
  });
990
- await mapRunToWorkflowInstance({
991
- env: input.env,
992
- runId: input.params.runId,
993
- instanceId: pooledInstanceId,
994
- });
995
- input.recordSubmitTiming({
996
- phase: 'coordinator.workflow_pool_send_event',
997
- ms: Date.now() - sendStartedAt,
998
- graphHash: input.params.graphHash ?? null,
999
- extra: { instanceId: pooledInstanceId },
1000
- });
1001
- return instance;
1002
1254
  } catch (error) {
1003
1255
  disposeRpcStub(instance);
1004
1256
  console.warn('[coordinator.workflow_pool] sendEvent failed; falling back', {
@@ -1008,6 +1260,13 @@ async function submitViaPooledWorkflow(input: {
1008
1260
  });
1009
1261
  return null;
1010
1262
  }
1263
+ input.recordSubmitTiming({
1264
+ phase: 'coordinator.workflow_pool_send_event',
1265
+ ms: Date.now() - sendStartedAt,
1266
+ graphHash: input.params.graphHash ?? null,
1267
+ extra: { instanceId: pooledInstanceId },
1268
+ });
1269
+ return instance;
1011
1270
  }
1012
1271
 
1013
1272
  function readWorkflowPayload(event: unknown): Record<string, unknown> | null {
@@ -1371,6 +1630,7 @@ function runRequestFromPlayWorkflowParams(params: PlayWorkflowParams): Record<st
1371
1630
  executionPlan: params.executionPlan ?? null,
1372
1631
  childPlayManifests: params.childPlayManifests ?? null,
1373
1632
  playCallGovernance: params.playCallGovernance ?? null,
1633
+ preloadedDbSessions: params.preloadedDbSessions ?? null,
1374
1634
  coordinatorUrl: params.coordinatorUrl ?? null,
1375
1635
  totalRows: params.totalRows,
1376
1636
  };
@@ -1725,6 +1985,13 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
1725
1985
  ? this.env.DEEPLINE_API_BASE_URL.trim()
1726
1986
  : 'https://code.deepline.com';
1727
1987
  const target = new URL(incoming.pathname + incoming.search, apiBaseUrl);
1988
+ const runtimeStatusBody =
1989
+ incoming.pathname === '/api/v2/plays/internal/runtime'
1990
+ ? await request
1991
+ .clone()
1992
+ .json()
1993
+ .catch(() => null)
1994
+ : null;
1728
1995
  const forwarded = new Request(target.toString(), request);
1729
1996
  const bypassToken = this.env.VERCEL_PROTECTION_BYPASS_TOKEN;
1730
1997
  if (typeof bypassToken === 'string' && bypassToken) {
@@ -1740,9 +2007,41 @@ export class RuntimeApi extends WorkerEntrypoint<CoordinatorEnv, undefined> {
1740
2007
  `[RUNTIME_API] ${incoming.pathname} failed: status=${res.status} ` +
1741
2008
  `target=${target.toString()} body=${body.slice(0, 500)}`,
1742
2009
  );
2010
+ } else {
2011
+ await this.recordRuntimeStatusEvent(runtimeStatusBody).catch(() => null);
1743
2012
  }
1744
2013
  return res;
1745
2014
  }
2015
+
2016
+ private async recordRuntimeStatusEvent(body: unknown): Promise<void> {
2017
+ if (!isRecord(body) || body.action !== 'update_run_status') {
2018
+ return;
2019
+ }
2020
+ const runId = typeof body.playId === 'string' ? body.playId : '';
2021
+ const status = typeof body.status === 'string' ? body.status : '';
2022
+ if (!runId || !status) {
2023
+ return;
2024
+ }
2025
+ await appendCoordinatorRunEvent(this.env, {
2026
+ runId,
2027
+ type: 'progress',
2028
+ status,
2029
+ ts: Date.now(),
2030
+ logs: Array.isArray(body.liveLogs)
2031
+ ? body.liveLogs.filter((line): line is string => typeof line === 'string')
2032
+ : undefined,
2033
+ activeNodeId:
2034
+ typeof body.activeNodeId === 'string' ? body.activeNodeId : null,
2035
+ activeArtifactTableNamespace:
2036
+ typeof body.activeArtifactTableNamespace === 'string'
2037
+ ? body.activeArtifactTableNamespace
2038
+ : null,
2039
+ updatedAt:
2040
+ typeof body.lastCheckpointAt === 'number'
2041
+ ? body.lastCheckpointAt
2042
+ : null,
2043
+ });
2044
+ }
1746
2045
  }
1747
2046
 
1748
2047
  export class CoordinatorControl extends WorkerEntrypoint<
@@ -1810,6 +2109,13 @@ export class CoordinatorControl extends WorkerEntrypoint<
1810
2109
  }
1811
2110
  await appendCoordinatorPerfTrace(this.env, payload);
1812
2111
  }
2112
+
2113
+ async recordRunEvent(runId: string, event: CoordinatorRunEvent): Promise<void> {
2114
+ if (!runId || event.runId !== runId) {
2115
+ throw new Error('Run event runId mismatch.');
2116
+ }
2117
+ await appendCoordinatorRunEvent(this.env, event);
2118
+ }
1813
2119
  }
1814
2120
 
1815
2121
  /**
@@ -1844,6 +2150,7 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
1844
2150
  });
1845
2151
  let dispatchedEvent = event;
1846
2152
  if (isPooledWorkflowBootstrapPayload(workflowEvent.payload)) {
2153
+ const pooledPayload = workflowEvent.payload;
1847
2154
  const waitingStep = step as {
1848
2155
  waitForEvent<T>(
1849
2156
  name: string,
@@ -1851,10 +2158,19 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
1851
2158
  ): Promise<{ payload: Readonly<T>; timestamp: Date; type: string }>;
1852
2159
  };
1853
2160
  const waitStartedAt = Date.now();
1854
- const startEvent = await waitingStep.waitForEvent<DispatcherEnvelope>(
2161
+ const startEventPromise = waitingStep.waitForEvent<DispatcherEnvelope>(
1855
2162
  'wait for pooled play start',
1856
2163
  { type: WORKFLOW_POOL_START_EVENT_TYPE, timeout: '10 minutes' },
1857
2164
  );
2165
+ await markWorkflowPoolIdReady(this.env, pooledPayload.poolId).catch(
2166
+ (error) => {
2167
+ console.warn('[coordinator.workflow_pool] ready signal failed', {
2168
+ poolId: pooledPayload.poolId,
2169
+ message: error instanceof Error ? error.message : String(error),
2170
+ });
2171
+ },
2172
+ );
2173
+ const startEvent = await startEventPromise;
1858
2174
  dispatchedEvent = {
1859
2175
  payload: startEvent.payload,
1860
2176
  timestamp: startEvent.timestamp,
@@ -1956,6 +2272,18 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
1956
2272
  run(e: unknown, s: unknown): Promise<unknown>;
1957
2273
  }
1958
2274
  ).run(innerEvent, innerStep);
2275
+ const output = isRecord(result) ? result : null;
2276
+ await writeCoordinatorTerminalState(env, {
2277
+ runId: runIdForTrace,
2278
+ status: 'completed',
2279
+ result: output?.result ?? result,
2280
+ totalRows: output?.totalRows ?? output?.outputRows ?? null,
2281
+ durationMs: output?.durationMs ?? null,
2282
+ playName:
2283
+ typeof output?.playName === 'string'
2284
+ ? output.playName
2285
+ : null,
2286
+ });
1959
2287
  trace({
1960
2288
  runId: runIdForTrace,
1961
2289
  phase: 'coordinator.runner_run',
@@ -1993,6 +2321,14 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
1993
2321
  },
1994
2322
  );
1995
2323
  });
2324
+ await writeCoordinatorTerminalState(env, {
2325
+ runId: runIdForTrace,
2326
+ status: 'failed',
2327
+ error:
2328
+ innerError instanceof Error
2329
+ ? innerError.message
2330
+ : String(innerError),
2331
+ }).catch(() => undefined);
1996
2332
  throw innerError;
1997
2333
  }
1998
2334
  },
@@ -2006,7 +2342,7 @@ const coordinatorEntrypoint = {
2006
2342
  /**
2007
2343
  * HTTP entrypoint for the Vercel app to dispatch into. Routes:
2008
2344
  * POST /workflow/{runId}/submit → PLAY_WORKFLOW.create({ id, params })
2009
- * GET /workflow/{runId}/observe polling-compatible status snapshot
2345
+ * GET /workflow/{runId}/tail ordered live run events after a cursor
2010
2346
  * POST /workflow/{runId}/cancel → Workflow instance terminate
2011
2347
  * POST /workflow/{runId}/signal → integration_event
2012
2348
  * GET /workflow/{runId}/result → terminal envelope
@@ -2029,6 +2365,34 @@ const coordinatorEntrypoint = {
2029
2365
  if (authError) return authError;
2030
2366
  return await handleCoordinatorWarmup(request, env, ctx);
2031
2367
  }
2368
+ if (url.pathname === '/tail-log-token/probe') {
2369
+ const authError = authorizeCoordinatorControlRequest({ request, env });
2370
+ if (authError) return authError;
2371
+ const expectedTailLogToken = env.DEEPLINE_TAIL_LOG_TOKEN?.trim();
2372
+ if (!expectedTailLogToken) {
2373
+ return Response.json(
2374
+ { ok: false, error: 'tail log token is not configured' },
2375
+ { status: 503 },
2376
+ );
2377
+ }
2378
+ const actualTailLogToken =
2379
+ request.headers.get('x-deepline-tail-log-token')?.trim() ?? '';
2380
+ if (actualTailLogToken !== expectedTailLogToken) {
2381
+ return Response.json(
2382
+ { ok: false, error: 'tail log token mismatch' },
2383
+ { status: 401 },
2384
+ );
2385
+ }
2386
+ return Response.json({
2387
+ ok: true,
2388
+ deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
2389
+ });
2390
+ }
2391
+ if (url.pathname === '/staged-files/put') {
2392
+ const authError = authorizeCoordinatorControlRequest({ request, env });
2393
+ if (authError) return authError;
2394
+ return await handleStagedFilePut(request, env);
2395
+ }
2032
2396
  if (url.pathname === '/workflow-pool/refill') {
2033
2397
  const internalAuthError = authorizeCoordinatorControlRequest({
2034
2398
  request,
@@ -2083,6 +2447,33 @@ const coordinatorEntrypoint = {
2083
2447
  ms: Date.now() - startedAt,
2084
2448
  });
2085
2449
  }
2450
+ if (url.pathname === '/workflow-pool/debug') {
2451
+ const internalAuthError = authorizeCoordinatorControlRequest({
2452
+ request,
2453
+ env,
2454
+ });
2455
+ if (internalAuthError) return internalAuthError;
2456
+ const entries = await listWorkflowPoolEntries(env);
2457
+ const detailed = [];
2458
+ for (const entry of entries) {
2459
+ const instance = await env.PLAY_WORKFLOW.get(entry.id);
2460
+ try {
2461
+ const status = await instance.status().catch(() => null);
2462
+ detailed.push({
2463
+ ...entry,
2464
+ status: workflowStatusName(status),
2465
+ mappedStatus: status ? mapWorkflowStatus(status) : 'running',
2466
+ });
2467
+ } finally {
2468
+ disposeRpcStub(instance);
2469
+ }
2470
+ }
2471
+ return Response.json({
2472
+ ok: true,
2473
+ enabled: workflowPoolEnabled(),
2474
+ entries: detailed,
2475
+ });
2476
+ }
2086
2477
 
2087
2478
  // Workflow routes: /workflow/{runId}/{action}
2088
2479
  const wfMatch = url.pathname.match(/^\/workflow\/([^/]+)(?:\/(.+))?$/);
@@ -2294,45 +2685,54 @@ async function handleWorkflowRoute(input: {
2294
2685
  let instance: WorkflowInstance | null = null;
2295
2686
  try {
2296
2687
  const dispatchStartedAt = Date.now();
2297
- const poolStartedAt = Date.now();
2298
- instance = await submitViaPooledWorkflow({
2688
+ recordSubmitTiming({
2689
+ phase: 'coordinator.workflow_pool_attempt',
2690
+ ms: 0,
2691
+ graphHash: params.graphHash ?? null,
2692
+ extra: {
2693
+ usedPool: false,
2694
+ disabled: true,
2695
+ reason: WORKFLOW_POOL_DISABLED_REASON,
2696
+ },
2697
+ });
2698
+ const createStartedAt = Date.now();
2699
+ instance = await createDynamicWorkflowInstance({
2299
2700
  env,
2701
+ id: defaultInstanceId,
2300
2702
  params,
2301
- recordSubmitTiming,
2302
2703
  });
2303
- const usedWorkflowPool = Boolean(instance);
2304
2704
  recordSubmitTiming({
2305
- phase: 'coordinator.workflow_pool_attempt',
2306
- ms: Date.now() - poolStartedAt,
2705
+ phase: 'coordinator.workflow_create',
2706
+ ms: Date.now() - createStartedAt,
2307
2707
  graphHash: params.graphHash ?? null,
2308
- extra: { usedPool: usedWorkflowPool },
2708
+ extra: { instanceId: instance.id },
2309
2709
  });
2310
- if (!instance) {
2311
- const createStartedAt = Date.now();
2312
- instance = await createDynamicWorkflowInstance({
2313
- env,
2314
- id: defaultInstanceId,
2315
- params,
2316
- });
2317
- recordSubmitTiming({
2318
- phase: 'coordinator.workflow_create',
2319
- ms: Date.now() - createStartedAt,
2320
- graphHash: params.graphHash ?? null,
2321
- extra: { instanceId: instance.id, pooled: false },
2322
- });
2323
- } else {
2324
- recordSubmitTiming({
2325
- phase: 'coordinator.workflow_create',
2326
- ms: 0,
2327
- graphHash: params.graphHash ?? null,
2328
- extra: { instanceId: instance.id, pooled: true },
2329
- });
2330
- }
2331
2710
  recordSubmitTiming({
2332
2711
  phase: 'coordinator.dispatch_workflow',
2333
2712
  ms: Date.now() - dispatchStartedAt,
2334
2713
  graphHash: params.graphHash ?? null,
2335
- extra: { pooled: usedWorkflowPool },
2714
+ extra: { startMode: 'direct_workflow_create' },
2715
+ });
2716
+ const initialWaitMsRaw = Number(
2717
+ new URL(request.url).searchParams.get('initialWaitMs') ?? '0',
2718
+ );
2719
+ const initialStateStartedAt = Date.now();
2720
+ const instanceState = await waitForSubmitInitialState({
2721
+ instance,
2722
+ runId: submittedRunId,
2723
+ waitMs: Number.isFinite(initialWaitMsRaw) ? initialWaitMsRaw : 0,
2724
+ });
2725
+ recordSubmitTiming({
2726
+ phase: 'coordinator.submit_initial_state',
2727
+ ms: Date.now() - initialStateStartedAt,
2728
+ graphHash: params.graphHash ?? null,
2729
+ extra: {
2730
+ waitMs: Number.isFinite(initialWaitMsRaw) ? initialWaitMsRaw : 0,
2731
+ status:
2732
+ typeof instanceState?.status === 'string'
2733
+ ? instanceState.status
2734
+ : null,
2735
+ },
2336
2736
  });
2337
2737
  const totalMs = Date.now() - submitStartedAt;
2338
2738
  recordSubmitTiming({
@@ -2348,11 +2748,11 @@ async function handleWorkflowRoute(input: {
2348
2748
  return Response.json({
2349
2749
  runId,
2350
2750
  status: 'submitted',
2351
- instanceState: null,
2751
+ workflowInstanceId: instance.id,
2752
+ instanceState,
2352
2753
  coordinatorTimings,
2353
2754
  });
2354
2755
  } finally {
2355
- input.ctx?.waitUntil(refillWorkflowPool(env).catch(() => undefined));
2356
2756
  disposeRpcStub(instance);
2357
2757
  }
2358
2758
  }
@@ -2582,12 +2982,84 @@ async function handleWorkflowRoute(input: {
2582
2982
  }
2583
2983
  }
2584
2984
 
2985
+ if (action === 'tail') {
2986
+ const url = new URL(request.url);
2987
+ const waitMs = Math.min(
2988
+ Math.max(Number(url.searchParams.get('waitMs') ?? '0'), 0),
2989
+ 30_000,
2990
+ );
2991
+ const afterSeq = Math.max(
2992
+ 0,
2993
+ Math.floor(Number(url.searchParams.get('afterSeq') ?? '0')),
2994
+ );
2995
+ const includeTrace = url.searchParams.get('trace') === '1';
2996
+ const statusStartedAt = Date.now();
2997
+ const eventResult = await listCoordinatorRunEvents({
2998
+ env,
2999
+ runId,
3000
+ afterSeq,
3001
+ timeoutMs: waitMs,
3002
+ }).catch(() => null);
3003
+ const coordinatorTrace =
3004
+ includeTrace && eventResult?.events.length
3005
+ ? await listCoordinatorPerfTrace(env, runId).catch(() => [])
3006
+ : [];
3007
+ const terminalEvent = eventResult?.events.find(
3008
+ (event): event is Extract<CoordinatorRunEvent, { type: 'terminal' }> =>
3009
+ event.type === 'terminal',
3010
+ );
3011
+ if (terminalEvent) {
3012
+ return Response.json({
3013
+ runId,
3014
+ ...(terminalEvent.playName ? { playName: terminalEvent.playName } : {}),
3015
+ status: terminalEvent.status,
3016
+ result: terminalEvent.result ?? null,
3017
+ error: terminalEvent.error ?? null,
3018
+ totalRows: terminalEvent.totalRows ?? null,
3019
+ durationMs: terminalEvent.durationMs ?? null,
3020
+ events: eventResult?.events ?? [],
3021
+ latestSeq: eventResult?.latestSeq ?? afterSeq,
3022
+ wait: null,
3023
+ coordinatorObserve: {
3024
+ ms: Date.now() - statusStartedAt,
3025
+ waitMs,
3026
+ workflowStatus: 'terminal-event',
3027
+ statusPolls: 0,
3028
+ instanceId: null,
3029
+ },
3030
+ ...(includeTrace ? { coordinatorTrace } : {}),
3031
+ });
3032
+ }
3033
+ return Response.json({
3034
+ runId,
3035
+ status: 'running',
3036
+ events: eventResult?.events ?? [],
3037
+ latestSeq: eventResult?.latestSeq ?? afterSeq,
3038
+ wait: null,
3039
+ coordinatorObserve: {
3040
+ ms: Date.now() - statusStartedAt,
3041
+ waitMs,
3042
+ workflowStatus:
3043
+ eventResult?.events.length ? 'event' : 'event-timeout',
3044
+ statusPolls: 0,
3045
+ instanceId: null,
3046
+ },
3047
+ ...(includeTrace ? { coordinatorTrace } : {}),
3048
+ });
3049
+ }
3050
+
2585
3051
  // get() throws if the instance doesn't exist (Workflows local-mode wipes
2586
3052
  // state on wrangler dev reload, and superseded `--force` runs may target
2587
3053
  // an instance that was never created). Treat that as a no-op cancel.
2588
3054
  let instance: WorkflowInstance | null = null;
2589
3055
  try {
2590
- const instanceId = await resolveWorkflowInstanceIdForRun(env, runId);
3056
+ const requestedInstanceId = new URL(request.url).searchParams
3057
+ .get('instanceId')
3058
+ ?.trim();
3059
+ const instanceId =
3060
+ requestedInstanceId && !isWorkflowMutatingAction(action)
3061
+ ? requestedInstanceId
3062
+ : await resolveWorkflowInstanceIdForRun(env, runId);
2591
3063
  instance = await env.PLAY_WORKFLOW.get(instanceId);
2592
3064
  } catch (error) {
2593
3065
  const message = error instanceof Error ? error.message : String(error);
@@ -2655,42 +3127,38 @@ async function handleWorkflowRoute(input: {
2655
3127
  });
2656
3128
  }
2657
3129
  if (
2658
- action === 'result' ||
2659
- action === 'status' ||
2660
- action === 'observe' ||
2661
- action === ''
3130
+ action === 'result' || action === 'status' || action === ''
2662
3131
  ) {
2663
- const observeWaitMs =
2664
- action === 'observe'
2665
- ? Math.min(
2666
- Math.max(
2667
- Number(new URL(request.url).searchParams.get('waitMs') ?? '0'),
2668
- 0,
2669
- ),
2670
- 2_000,
2671
- )
2672
- : 0;
2673
3132
  const includeTrace =
2674
3133
  new URL(request.url).searchParams.get('trace') === '1';
2675
3134
  const statusStartedAt = Date.now();
2676
- let status = await instance.status();
2677
- let statusPolls = 1;
2678
- while (
2679
- observeWaitMs > 0 &&
2680
- Date.now() - statusStartedAt < observeWaitMs
2681
- ) {
2682
- const result = mapWorkflowResult(runId, status);
2683
- if (
2684
- result.status === 'completed' ||
2685
- result.status === 'failed' ||
2686
- result.status === 'cancelled'
2687
- ) {
2688
- break;
2689
- }
2690
- await new Promise((resolve) => setTimeout(resolve, 75));
2691
- status = await instance.status();
2692
- statusPolls += 1;
3135
+ const terminalState = await readCoordinatorTerminalState(env, runId).catch(
3136
+ () => null,
3137
+ );
3138
+ if (terminalState) {
3139
+ const coordinatorTrace = includeTrace
3140
+ ? await listCoordinatorPerfTrace(env, runId).catch(() => [])
3141
+ : [];
3142
+ return Response.json({
3143
+ runId,
3144
+ ...(terminalState.playName ? { playName: terminalState.playName } : {}),
3145
+ status: terminalState.status,
3146
+ result: terminalState.result ?? null,
3147
+ error: terminalState.error ?? null,
3148
+ totalRows: terminalState.totalRows ?? null,
3149
+ durationMs: terminalState.durationMs ?? null,
3150
+ wait: null,
3151
+ coordinatorObserve: {
3152
+ ms: Date.now() - statusStartedAt,
3153
+ waitMs: 0,
3154
+ workflowStatus: 'terminal-cache',
3155
+ statusPolls: 0,
3156
+ instanceId: instance.id,
3157
+ },
3158
+ ...(includeTrace ? { coordinatorTrace } : {}),
3159
+ });
2693
3160
  }
3161
+ const status = await instance.status();
2694
3162
  const result = mapWorkflowResult(runId, status);
2695
3163
  const observeMs = Date.now() - statusStartedAt;
2696
3164
  // If we forced a permanent-error fail-fast (status='failed' even
@@ -2729,9 +3197,9 @@ async function handleWorkflowRoute(input: {
2729
3197
  ...result,
2730
3198
  coordinatorObserve: {
2731
3199
  ms: observeMs,
2732
- waitMs: observeWaitMs,
3200
+ waitMs: 0,
2733
3201
  workflowStatus: status.status,
2734
- statusPolls,
3202
+ statusPolls: 1,
2735
3203
  instanceId: instance.id,
2736
3204
  },
2737
3205
  ...(includeTrace ? { coordinatorTrace } : {}),
@@ -2760,6 +3228,54 @@ function stableHash(value: string): string {
2760
3228
  return (hash >>> 0).toString(36);
2761
3229
  }
2762
3230
 
3231
+ const DYNAMIC_PLAY_WORKER_HARNESS_VERSION =
3232
+ 'h6-runtime-api-coordinator-deploy-scoped';
3233
+ const DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES = 64;
3234
+ const dynamicWorkerBundledCodeCache = new Map<string, string>();
3235
+
3236
+ function dynamicPlayWorkerCacheKey(input: {
3237
+ env: CoordinatorEnv;
3238
+ graphHash: string;
3239
+ artifactIdentity: string;
3240
+ }): string {
3241
+ const deployMarker =
3242
+ input.env.DEEPLINE_COORDINATOR_DEPLOY_MARKER?.trim() || 'local';
3243
+ return [
3244
+ 'play',
3245
+ input.graphHash,
3246
+ input.artifactIdentity,
3247
+ `harness=${DYNAMIC_PLAY_WORKER_HARNESS_VERSION}`,
3248
+ `deploy=${deployMarker}`,
3249
+ ].join(':');
3250
+ }
3251
+
3252
+ function dynamicWorkerBundledCodeCacheKey(input: {
3253
+ artifactStorageKey: string;
3254
+ artifactHash?: string | null;
3255
+ }): string {
3256
+ return `${input.artifactHash?.trim() || 'no-hash'}:${input.artifactStorageKey}`;
3257
+ }
3258
+
3259
+ function readDynamicWorkerBundledCodeCache(key: string): string | null {
3260
+ const cached = dynamicWorkerBundledCodeCache.get(key);
3261
+ if (cached === undefined) return null;
3262
+ dynamicWorkerBundledCodeCache.delete(key);
3263
+ dynamicWorkerBundledCodeCache.set(key, cached);
3264
+ return cached;
3265
+ }
3266
+
3267
+ function writeDynamicWorkerBundledCodeCache(key: string, code: string): void {
3268
+ dynamicWorkerBundledCodeCache.set(key, code);
3269
+ while (
3270
+ dynamicWorkerBundledCodeCache.size >
3271
+ DYNAMIC_WORKER_BUNDLED_CODE_CACHE_MAX_ENTRIES
3272
+ ) {
3273
+ const oldestKey = dynamicWorkerBundledCodeCache.keys().next().value;
3274
+ if (typeof oldestKey !== 'string') break;
3275
+ dynamicWorkerBundledCodeCache.delete(oldestKey);
3276
+ }
3277
+ }
3278
+
2763
3279
  /**
2764
3280
  * Synchronous wrapper around env.LOADER.get for use inside the
2765
3281
  * createDynamicWorkflowEntrypoint loader callback. The framework's loader
@@ -2798,7 +3314,11 @@ function loadDynamicPlayWorkerSync(
2798
3314
  }
2799
3315
  const artifactIdentity =
2800
3316
  metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
2801
- const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=h4-runtime-api`;
3317
+ const workerCacheKey = dynamicPlayWorkerCacheKey({
3318
+ env,
3319
+ graphHash,
3320
+ artifactIdentity,
3321
+ });
2802
3322
  const runIdForTrace = metadata.runId ?? graphHash;
2803
3323
  const loaderGetStartedAt = Date.now();
2804
3324
  const stub = env.LOADER.get(workerCacheKey, async () => {
@@ -2829,6 +3349,10 @@ function loadDynamicPlayWorkerSync(
2829
3349
  // tool execution, DB session, and artifact callbacks. This avoids a
2830
3350
  // public fetch hop when Cloudflare exposes the RuntimeApi export.
2831
3351
  ...makeRuntimeApiEnvBinding(),
3352
+ // In-process coordinator control bridge used by ctx.runPlay and
3353
+ // parent terminal signals. This keeps scalar child plays inline with
3354
+ // the parent instead of round-tripping through nested Workflow waits.
3355
+ ...makeCoordinatorControlBinding(),
2832
3356
  // NOTE: We intentionally do NOT pass `env.PLAYS_BUCKET` (an R2Bucket
2833
3357
  // binding) through to the per-play Worker's env. Including a raw
2834
3358
  // R2Bucket in the dynamically-loaded Worker's env makes Cloudflare
@@ -2880,7 +3404,11 @@ async function loadDynamicPlayWorker(
2880
3404
  }
2881
3405
  const artifactIdentity =
2882
3406
  metadata.artifactHash?.trim() || stableHash(artifactStorageKey);
2883
- const workerCacheKey = `play:${graphHash}:${artifactIdentity}:harness=h4-runtime-api`;
3407
+ const workerCacheKey = dynamicPlayWorkerCacheKey({
3408
+ env,
3409
+ graphHash,
3410
+ artifactIdentity,
3411
+ });
2884
3412
  const runIdForTrace = metadata.runId ?? graphHash;
2885
3413
  const loaderGetStartedAt = Date.now();
2886
3414
  const stub = env.LOADER.get(workerCacheKey, async () => {
@@ -2902,11 +3430,11 @@ async function loadDynamicPlayWorker(
2902
3430
  // Mirror of the sync loader (above) — see that copy for the
2903
3431
  // architectural rationale. The dynamic worker env is intentionally
2904
3432
  // minimal; runtime callbacks use RUNTIME_API, file reads go through
2905
- // HARNESS, and child workflow control uses the coordinator URL in the
2906
- // run request.
3433
+ // HARNESS, and child workflow control uses the COORDINATOR binding.
2907
3434
  HARNESS: env.HARNESS,
2908
3435
  VERCEL_PROTECTION_BYPASS_TOKEN: env.VERCEL_PROTECTION_BYPASS_TOKEN,
2909
3436
  ...makeRuntimeApiEnvBinding(),
3437
+ ...makeCoordinatorControlBinding(),
2910
3438
  },
2911
3439
  };
2912
3440
  });
@@ -2930,31 +3458,42 @@ async function loadDynamicWorkerBundledCode(input: {
2930
3458
  trace: CoordinatorPerfTraceSink;
2931
3459
  }): Promise<string> {
2932
3460
  const callbackStartedAt = Date.now();
2933
- let codeSource: 'inline' | 'r2' = 'inline';
3461
+ let codeSource: 'inline' | 'r2' | 'memory' = 'inline';
2934
3462
  let r2Ms = 0;
2935
- const artifact = input.metadata.dynamicWorkerCode
2936
- ? null
2937
- : await (async () => {
2938
- codeSource = 'r2';
2939
- const r2StartedAt = Date.now();
2940
- try {
2941
- return await loadStoredPlayArtifactFromR2(
2942
- input.env,
2943
- input.artifactStorageKey,
2944
- );
2945
- } finally {
2946
- r2Ms = Date.now() - r2StartedAt;
2947
- input.trace({
2948
- runId: input.runIdForTrace,
2949
- phase: 'coordinator.loader_code_r2_get',
2950
- ms: r2Ms,
2951
- graphHash: input.graphHash,
2952
- extra: { artifactStorageKey: input.artifactStorageKey },
2953
- });
2954
- }
2955
- })();
2956
- const bundledCode =
2957
- input.metadata.dynamicWorkerCode ?? artifact?.artifact?.bundledCode;
3463
+ const codeCacheKey = dynamicWorkerBundledCodeCacheKey({
3464
+ artifactStorageKey: input.artifactStorageKey,
3465
+ artifactHash: input.metadata.artifactHash,
3466
+ });
3467
+ let bundledCode = input.metadata.dynamicWorkerCode ?? null;
3468
+ if (!bundledCode) {
3469
+ bundledCode = readDynamicWorkerBundledCodeCache(codeCacheKey);
3470
+ if (bundledCode) {
3471
+ codeSource = 'memory';
3472
+ }
3473
+ }
3474
+ if (!bundledCode) {
3475
+ codeSource = 'r2';
3476
+ const r2StartedAt = Date.now();
3477
+ try {
3478
+ const artifact = await loadStoredPlayArtifactFromR2(
3479
+ input.env,
3480
+ input.artifactStorageKey,
3481
+ );
3482
+ bundledCode = artifact?.artifact?.bundledCode ?? null;
3483
+ if (typeof bundledCode === 'string' && bundledCode.length > 0) {
3484
+ writeDynamicWorkerBundledCodeCache(codeCacheKey, bundledCode);
3485
+ }
3486
+ } finally {
3487
+ r2Ms = Date.now() - r2StartedAt;
3488
+ input.trace({
3489
+ runId: input.runIdForTrace,
3490
+ phase: 'coordinator.loader_code_r2_get',
3491
+ ms: r2Ms,
3492
+ graphHash: input.graphHash,
3493
+ extra: { artifactStorageKey: input.artifactStorageKey },
3494
+ });
3495
+ }
3496
+ }
2958
3497
  if (typeof bundledCode !== 'string' || bundledCode.length === 0) {
2959
3498
  throw new Error(
2960
3499
  `Stored play artifact ${input.artifactStorageKey} does not contain bundledCode.`,
@@ -3046,6 +3585,192 @@ export default {
3046
3585
  };
3047
3586
  `;
3048
3587
 
3588
+ function isAllowedStagedFileStorageKey(key: string): boolean {
3589
+ if (!key || key.length > 2_048) return false;
3590
+ if (key.startsWith('/') || key.includes('..') || key.includes('\\')) {
3591
+ return false;
3592
+ }
3593
+ return key.startsWith('plays/v2/orgs/') || key.includes('/plays/v2/orgs/');
3594
+ }
3595
+
3596
+ function decodeBase64ToUint8Array(value: string): Uint8Array {
3597
+ const binary = atob(value);
3598
+ const bytes = new Uint8Array(binary.length);
3599
+ for (let index = 0; index < binary.length; index += 1) {
3600
+ bytes[index] = binary.charCodeAt(index);
3601
+ }
3602
+ return bytes;
3603
+ }
3604
+
3605
+ async function handleStagedFilePut(
3606
+ request: Request,
3607
+ env: CoordinatorEnv,
3608
+ ): Promise<Response> {
3609
+ if (request.method !== 'POST') {
3610
+ return new Response('method not allowed', { status: 405 });
3611
+ }
3612
+ const url = new URL(request.url);
3613
+ const rawKey = url.searchParams.get('key')?.trim() ?? '';
3614
+ if (rawKey) {
3615
+ const key = rawKey;
3616
+ const contentType =
3617
+ url.searchParams.get('contentType')?.trim() ||
3618
+ 'application/octet-stream';
3619
+ const expectedBytes = Number(url.searchParams.get('bytes') ?? 'NaN');
3620
+ if (!isAllowedStagedFileStorageKey(key)) {
3621
+ return Response.json(
3622
+ { error: 'invalid staged file key' },
3623
+ { status: 400 },
3624
+ );
3625
+ }
3626
+ if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) {
3627
+ return Response.json({ error: 'bytes is required' }, { status: 400 });
3628
+ }
3629
+ const existing = await headExistingStagedFile(env, key, expectedBytes);
3630
+ if (existing.exists) {
3631
+ console.info('[perf][coordinator.staged_file_put]', {
3632
+ key,
3633
+ bytes: expectedBytes,
3634
+ headMs: existing.ms,
3635
+ putMs: 0,
3636
+ ms: existing.ms,
3637
+ transport: 'raw',
3638
+ skipped: true,
3639
+ });
3640
+ return Response.json({
3641
+ ok: true,
3642
+ key,
3643
+ bytes: expectedBytes,
3644
+ existed: true,
3645
+ timingsMs: { head: existing.ms, put: 0 },
3646
+ });
3647
+ }
3648
+ const readStartedAt = Date.now();
3649
+ const bytes = new Uint8Array(await request.arrayBuffer());
3650
+ const readMs = Date.now() - readStartedAt;
3651
+ if (bytes.byteLength !== expectedBytes) {
3652
+ return Response.json(
3653
+ { error: 'staged file byte length mismatch' },
3654
+ { status: 400 },
3655
+ );
3656
+ }
3657
+ const putStartedAt = Date.now();
3658
+ await env.PLAYS_BUCKET.put(key, bytes, {
3659
+ httpMetadata: { contentType },
3660
+ });
3661
+ const putMs = Date.now() - putStartedAt;
3662
+ console.info('[perf][coordinator.staged_file_put]', {
3663
+ key,
3664
+ bytes: bytes.byteLength,
3665
+ readMs,
3666
+ putMs,
3667
+ ms: readMs + putMs,
3668
+ transport: 'raw',
3669
+ });
3670
+ return Response.json({
3671
+ ok: true,
3672
+ key,
3673
+ bytes: bytes.byteLength,
3674
+ timingsMs: { read: readMs, put: putMs },
3675
+ });
3676
+ }
3677
+ const body = (await request.json().catch(() => null)) as
3678
+ | {
3679
+ key?: unknown;
3680
+ contentBase64?: unknown;
3681
+ contentType?: unknown;
3682
+ bytes?: unknown;
3683
+ }
3684
+ | null;
3685
+ const key = typeof body?.key === 'string' ? body.key.trim() : '';
3686
+ const contentBase64 =
3687
+ typeof body?.contentBase64 === 'string' ? body.contentBase64 : '';
3688
+ const contentType =
3689
+ typeof body?.contentType === 'string' && body.contentType.trim()
3690
+ ? body.contentType.trim()
3691
+ : 'application/octet-stream';
3692
+ const expectedBytes = typeof body?.bytes === 'number' ? body.bytes : NaN;
3693
+ if (!isAllowedStagedFileStorageKey(key)) {
3694
+ return Response.json(
3695
+ { error: 'invalid staged file key' },
3696
+ { status: 400 },
3697
+ );
3698
+ }
3699
+ if (
3700
+ !contentBase64 ||
3701
+ !Number.isSafeInteger(expectedBytes) ||
3702
+ expectedBytes < 0
3703
+ ) {
3704
+ return Response.json(
3705
+ { error: 'contentBase64 and bytes are required' },
3706
+ { status: 400 },
3707
+ );
3708
+ }
3709
+ const existing = await headExistingStagedFile(env, key, expectedBytes);
3710
+ if (existing.exists) {
3711
+ console.info('[perf][coordinator.staged_file_put]', {
3712
+ key,
3713
+ bytes: expectedBytes,
3714
+ headMs: existing.ms,
3715
+ putMs: 0,
3716
+ ms: existing.ms,
3717
+ transport: 'base64',
3718
+ skipped: true,
3719
+ });
3720
+ return Response.json({
3721
+ ok: true,
3722
+ key,
3723
+ bytes: expectedBytes,
3724
+ existed: true,
3725
+ timingsMs: { head: existing.ms, put: 0 },
3726
+ });
3727
+ }
3728
+ const decodeStartedAt = Date.now();
3729
+ const bytes = decodeBase64ToUint8Array(contentBase64);
3730
+ const decodeMs = Date.now() - decodeStartedAt;
3731
+ if (bytes.byteLength !== expectedBytes) {
3732
+ return Response.json(
3733
+ { error: 'staged file byte length mismatch' },
3734
+ { status: 400 },
3735
+ );
3736
+ }
3737
+ const putStartedAt = Date.now();
3738
+ await env.PLAYS_BUCKET.put(key, bytes, {
3739
+ httpMetadata: { contentType },
3740
+ });
3741
+ const putMs = Date.now() - putStartedAt;
3742
+ console.info('[perf][coordinator.staged_file_put]', {
3743
+ key,
3744
+ bytes: bytes.byteLength,
3745
+ decodeMs,
3746
+ putMs,
3747
+ ms: decodeMs + putMs,
3748
+ });
3749
+ return Response.json({
3750
+ ok: true,
3751
+ key,
3752
+ bytes: bytes.byteLength,
3753
+ timingsMs: { decode: decodeMs, put: putMs },
3754
+ });
3755
+ }
3756
+
3757
+ async function headExistingStagedFile(
3758
+ env: CoordinatorEnv,
3759
+ key: string,
3760
+ expectedBytes: number,
3761
+ ): Promise<{ exists: boolean; ms: number }> {
3762
+ const startedAt = Date.now();
3763
+ const object = await env.PLAYS_BUCKET.head(key).catch(() => null);
3764
+ const ms = Date.now() - startedAt;
3765
+ if (!object) {
3766
+ return { exists: false, ms };
3767
+ }
3768
+ if (typeof object.size === 'number' && object.size !== expectedBytes) {
3769
+ return { exists: false, ms };
3770
+ }
3771
+ return { exists: true, ms };
3772
+ }
3773
+
3049
3774
  async function handleCoordinatorWarmup(
3050
3775
  request: Request,
3051
3776
  env: CoordinatorEnv,
@@ -3219,6 +3944,7 @@ async function handleCoordinatorWarmup(
3219
3944
  * worker uses its existing `fetch(req.baseUrl + path)` transport.
3220
3945
  */
3221
3946
  let loggedMissingRuntimeApiExport = false;
3947
+ let loggedMissingCoordinatorControlExport = false;
3222
3948
 
3223
3949
  function makeRuntimeApiEnvBinding():
3224
3950
  | { RUNTIME_API: { fetch(req: Request): Promise<Response> } }
@@ -3241,20 +3967,28 @@ function makeRuntimeApiEnvBinding():
3241
3967
  return { RUNTIME_API: ctor({ props: undefined }) };
3242
3968
  }
3243
3969
 
3244
- function makeCoordinatorControlBinding(): {
3245
- submitChild(
3246
- parentRunId: string,
3247
- body: Record<string, unknown>,
3248
- ): Promise<{ workflowId?: string; runId?: string; error?: unknown }>;
3249
- signal(
3250
- runId: string,
3251
- body: Record<string, unknown>,
3252
- ): Promise<Record<string, unknown>>;
3253
- recordPerfTrace(
3254
- runId: string,
3255
- payload: CoordinatorPerfTracePayload,
3256
- ): Promise<void>;
3257
- } {
3970
+ function makeCoordinatorControlBinding():
3971
+ | {
3972
+ COORDINATOR: {
3973
+ submitChild(
3974
+ parentRunId: string,
3975
+ body: Record<string, unknown>,
3976
+ ): Promise<{ workflowId?: string; runId?: string; error?: unknown }>;
3977
+ signal(
3978
+ runId: string,
3979
+ body: Record<string, unknown>,
3980
+ ): Promise<Record<string, unknown>>;
3981
+ recordPerfTrace(
3982
+ runId: string,
3983
+ payload: CoordinatorPerfTracePayload,
3984
+ ): Promise<void>;
3985
+ recordRunEvent(
3986
+ runId: string,
3987
+ event: CoordinatorRunEvent,
3988
+ ): Promise<void>;
3989
+ };
3990
+ }
3991
+ | Record<string, never> {
3258
3992
  const exports = workersExports as unknown as {
3259
3993
  CoordinatorControl?: (init: { props: undefined }) => {
3260
3994
  submitChild(
@@ -3269,15 +4003,23 @@ function makeCoordinatorControlBinding(): {
3269
4003
  runId: string,
3270
4004
  payload: CoordinatorPerfTracePayload,
3271
4005
  ): Promise<void>;
4006
+ recordRunEvent(
4007
+ runId: string,
4008
+ event: CoordinatorRunEvent,
4009
+ ): Promise<void>;
3272
4010
  };
3273
4011
  };
3274
4012
  const ctor = exports.CoordinatorControl;
3275
4013
  if (typeof ctor !== 'function') {
3276
- throw new Error(
3277
- 'CoordinatorControl is not registered on cloudflare:workers exports.',
3278
- );
4014
+ if (!loggedMissingCoordinatorControlExport) {
4015
+ loggedMissingCoordinatorControlExport = true;
4016
+ console.warn(
4017
+ '[coordinator] CoordinatorControl is not registered on cloudflare:workers exports; using public coordinator transport.',
4018
+ );
4019
+ }
4020
+ return {};
3279
4021
  }
3280
- return ctor({ props: undefined });
4022
+ return { COORDINATOR: ctor({ props: undefined }) };
3281
4023
  }
3282
4024
 
3283
4025
  async function loadStoredPlayArtifactFromR2(