deepline 0.1.199 → 0.1.200

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 (72) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +5 -1
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +51 -17
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
  7. package/dist/bundling-sources/sdk/src/http.ts +5 -2
  8. package/dist/bundling-sources/sdk/src/play.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
  10. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  11. package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
  12. package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
  14. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
  15. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
  17. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
  18. package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
  22. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
  30. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
  31. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
  34. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +2 -3
  38. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +459 -103
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
  52. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
  53. package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
  55. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
  58. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
  59. package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
  60. package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
  61. package/dist/cli/index.js +167 -54
  62. package/dist/cli/index.mjs +167 -54
  63. package/dist/index.d.mts +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.js +8 -8
  66. package/dist/index.mjs +8 -8
  67. package/dist/plays/bundle-play-file.mjs +15 -20
  68. package/package.json +1 -1
  69. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
  70. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
  71. package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
  72. package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
@@ -0,0 +1,12 @@
1
+ /** Maximum active user-code runtime for a standard play, in seconds. */
2
+ export const STANDARD_PLAY_RUNTIME_LIMIT_SECONDS = 20 * 60;
3
+ export const STANDARD_PLAY_RUNTIME_LIMIT_LABEL = '20 minutes';
4
+
5
+ /**
6
+ * Runner timeout includes setup, cleanup, and billing headroom after the
7
+ * user-code runtime cap.
8
+ */
9
+ export const PLAY_RUNNER_TIMEOUT_SECONDS = 30 * 60;
10
+
11
+ /** TTL for workflow executor tokens, in seconds. */
12
+ export const WORKFLOW_EXECUTOR_TOKEN_TTL_SECONDS = PLAY_RUNNER_TIMEOUT_SECONDS;
@@ -0,0 +1,23 @@
1
+ export const DEEPLINE_V2_INFRA_MODE_ENV = 'DEEPLINE_V2_INFRA_MODE';
2
+
3
+ function currentRuntimeInfraMode(): string {
4
+ return process.env[DEEPLINE_V2_INFRA_MODE_ENV]?.trim().toLowerCase() ?? '';
5
+ }
6
+
7
+ export function isPreviewPlaysDeployment(): boolean {
8
+ const mode = currentRuntimeInfraMode();
9
+ return (
10
+ mode === 'external' ||
11
+ mode === 'preview' ||
12
+ mode === 'absurd-preview' ||
13
+ mode === 'absurd:preview'
14
+ );
15
+ }
16
+
17
+ export function isCloudflarePlaysDeployment(): boolean {
18
+ return currentRuntimeInfraMode() === 'cloudflare';
19
+ }
20
+
21
+ export function requiresPublicCallbackBaseUrl(): boolean {
22
+ return isPreviewPlaysDeployment() || isCloudflarePlaysDeployment();
23
+ }
@@ -57,6 +57,20 @@ function normalizeAttemptSeq(value: number | null | undefined): number {
57
57
  return Number.isFinite(value) ? Math.trunc(value as number) : 0;
58
58
  }
59
59
 
60
+ function isSameOwnerOlderRequest(input: {
61
+ row: RuntimeSheetRowAttemptInput;
62
+ request: RuntimeSheetAttemptIdentity;
63
+ }): boolean {
64
+ const rowOwner =
65
+ normalize(input.row.attemptOwnerRunId) ?? normalize(input.row.runId);
66
+ const requestOwner = normalize(input.request.attemptOwnerRunId);
67
+ return (
68
+ Boolean(rowOwner && requestOwner && rowOwner === requestOwner) &&
69
+ normalizeAttemptSeq(input.request.attemptSeq) <
70
+ normalizeAttemptSeq(input.row.attemptSeq)
71
+ );
72
+ }
73
+
60
74
  export function classifyRuntimeSheetAttemptRelation(input: {
61
75
  row: RuntimeSheetRowAttemptInput;
62
76
  request: RuntimeSheetAttemptIdentity;
@@ -103,13 +117,14 @@ export function decideRuntimeSheetPrepare(input: {
103
117
  }
104
118
  const relation = classifyRuntimeSheetAttemptRelation(input);
105
119
  if (input.row.status === 'pending' || input.row.status === 'running') {
106
- return relation.kind === 'other_active_owner' || relation.kind === 'malformed'
120
+ return (relation.kind !== 'expired' && isSameOwnerOlderRequest(input)) ||
121
+ relation.kind === 'malformed'
107
122
  ? { kind: 'blocked_by_active_owner' }
108
123
  : { kind: 'claim_executable' };
109
124
  }
110
125
  if (input.row.status === 'enriched') {
111
126
  if (!input.missingRequiredOutput) return { kind: 'reuse_completed' };
112
- return relation.kind === 'same_owner_stale_attempt' ||
127
+ return (relation.kind !== 'expired' && isSameOwnerOlderRequest(input)) ||
113
128
  relation.kind === 'malformed'
114
129
  ? { kind: 'blocked_by_active_owner' }
115
130
  : { kind: 'claim_executable' };
@@ -123,6 +138,12 @@ export function decideRuntimeSheetTerminalWrite(input: {
123
138
  nowMs?: number;
124
139
  }): RuntimeSheetTerminalDecision {
125
140
  const relation = classifyRuntimeSheetAttemptRelation(input);
141
+ if (input.row.status !== 'enriched') {
142
+ return (relation.kind !== 'expired' && isSameOwnerOlderRequest(input)) ||
143
+ relation.kind === 'malformed'
144
+ ? { kind: 'reject_stale_attempt' }
145
+ : { kind: 'write_terminal' };
146
+ }
126
147
  if (
127
148
  relation.kind === 'unowned' ||
128
149
  relation.kind === 'same_attempt' ||
@@ -5,7 +5,7 @@
5
5
  * Selected per-run via PlayExecutionProfile.
6
6
  *
7
7
  * Cloudflare Workflows is the default production scheduler through the
8
- * workers_edge profile. Hatchet is selected explicitly by the hatchet profile.
8
+ * workers_edge profile. Absurd is the durable Daytona-backed scheduler.
9
9
  *
10
10
  * Customer plays are unaffected — this is purely the orchestration layer.
11
11
  */
@@ -20,23 +20,25 @@ import type { PlayRuntimeManifestMap } from '../plays/compiler-manifest';
20
20
  import type { PreloadedRuntimeDbSession } from './db-session';
21
21
  import type { PlayRunnerRuntimeTiming } from './protocol';
22
22
  import type { RuntimeTestPolicyOverrides } from './test-runtime-seams';
23
+ import type { PlayRunInputPayload } from './play-input';
23
24
 
24
25
  export const PLAY_SCHEDULER_BACKENDS = {
25
- temporal: 'temporal',
26
26
  cfWorkflows: 'cf-workflows',
27
+ /** Private legacy id retained only so old persisted rows can be interpreted. */
28
+ postgres: 'postgres',
27
29
  /**
28
- * Private legacy id retained only so old persisted rows can be interpreted.
29
- * It is not a selectable scheduler backend after the Hatchet hard cutover.
30
+ * Postgres-native durable-execution scheduler (vendored Absurd engine). Runs
31
+ * on the battle-tested run-state store without an external workflow engine.
32
+ * Selected explicitly by the `absurd` profile.
30
33
  */
31
- postgres: 'postgres',
32
- hatchet: 'hatchet',
34
+ absurd: 'absurd',
33
35
  inProcess: 'in-process',
34
36
  } as const;
35
37
 
36
38
  export type PlaySchedulerBackendId =
37
39
  (typeof PLAY_SCHEDULER_BACKENDS)[keyof typeof PLAY_SCHEDULER_BACKENDS];
38
40
 
39
- export type PlayRunInputPayload = Record<string, unknown> | unknown[];
41
+ export type { PlayRunInputPayload };
40
42
 
41
43
  export type PlayCallGovernanceSnapshot = {
42
44
  rootRunId: string;
@@ -78,9 +80,8 @@ export type PlaySchedulerSubmitInput = {
78
80
  workflowFamilyKey?: string | null;
79
81
  artifactStorageKey: string;
80
82
  /**
81
- * Optional inline artifact for schedulers that run the legacy Temporal
82
- * workflow path. Workers-edge schedulers use artifactStorageKey plus
83
- * dynamicWorkerCode instead.
83
+ * Optional inline artifact for node-compatible schedulers. Workers-edge
84
+ * schedulers use artifactStorageKey plus dynamicWorkerCode instead.
84
85
  */
85
86
  runtimeArtifact?: unknown;
86
87
  artifactHash: string;
@@ -101,11 +102,6 @@ export type PlaySchedulerSubmitInput = {
101
102
  bytes?: number;
102
103
  } | null;
103
104
  inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
104
- /**
105
- * Temporal-only escape hatch for small legacy inputs. Workers-edge CSV
106
- * inputs must be staged and streamed by handle instead of carried as raw
107
- * scheduler payload bytes.
108
- */
109
105
  inlineInputFile?: {
110
106
  logicalPath: string;
111
107
  fileName: string;
@@ -143,8 +139,6 @@ export type PlaySchedulerSubmitInput = {
143
139
  userId?: string | null;
144
140
  source?: 'published' | 'ad_hoc' | 'draft';
145
141
  executionProfile?: string | null;
146
- /** Durable per-workspace active run cap enforced when the run row is projected. */
147
- activeRunLimit?: number | null;
148
142
  /** runner backend to use for executing attempts */
149
143
  runtimeBackend: string;
150
144
  /** dedup backend for cross-attempt cross-process idempotency */
@@ -244,24 +238,22 @@ export function normalizePlaySchedulerBackend(
244
238
  value?: string | null,
245
239
  ): PlaySchedulerBackendId {
246
240
  const normalized = value?.trim().toLowerCase();
247
- if (!normalized || normalized === 'temporal') {
248
- return PLAY_SCHEDULER_BACKENDS.temporal;
241
+ if (!normalized) {
242
+ return PLAY_SCHEDULER_BACKENDS.cfWorkflows;
249
243
  }
250
244
  if (normalized === 'cf-workflows' || normalized === 'cf_workflows') {
251
245
  return PLAY_SCHEDULER_BACKENDS.cfWorkflows;
252
246
  }
253
- if (normalized === 'hatchet' || normalized === 'hatchet-scheduler') {
254
- return PLAY_SCHEDULER_BACKENDS.hatchet;
247
+ if (normalized === 'absurd' || normalized === 'absurd-scheduler') {
248
+ return PLAY_SCHEDULER_BACKENDS.absurd;
255
249
  }
256
250
  if (normalized === 'in-process' || normalized === 'in_process') {
257
251
  return PLAY_SCHEDULER_BACKENDS.inProcess;
258
252
  }
259
253
  throw new Error(
260
254
  `Unsupported scheduler backend "${normalized}". Expected one of: ${[
261
- PLAY_SCHEDULER_BACKENDS.temporal,
262
255
  PLAY_SCHEDULER_BACKENDS.cfWorkflows,
263
- PLAY_SCHEDULER_BACKENDS.hatchet,
264
- PLAY_SCHEDULER_BACKENDS.inProcess,
256
+ PLAY_SCHEDULER_BACKENDS.absurd,
265
257
  ].join(', ')}.`,
266
258
  );
267
259
  }
@@ -0,0 +1,26 @@
1
+ export interface SerialTaskQueue {
2
+ enqueue<T>(task: () => Promise<T>): Promise<T>;
3
+ idle(): Promise<void>;
4
+ }
5
+
6
+ /**
7
+ * Runs asynchronous tasks one at a time in enqueue order. A rejected task is
8
+ * returned to its caller but does not poison the queue for later work.
9
+ */
10
+ export function createSerialTaskQueue(): SerialTaskQueue {
11
+ let tail: Promise<void> = Promise.resolve();
12
+
13
+ return {
14
+ enqueue<T>(task: () => Promise<T>): Promise<T> {
15
+ const result = tail.then(task);
16
+ tail = result.then(
17
+ () => undefined,
18
+ () => undefined,
19
+ );
20
+ return result;
21
+ },
22
+ idle(): Promise<void> {
23
+ return tail;
24
+ },
25
+ };
26
+ }
@@ -13,7 +13,7 @@
13
13
  * (and the workflow event history). Larger inputs must be staged via
14
14
  * `POST /api/v2/plays/files/stage` and referenced by handle.
15
15
  */
16
- export const MAX_TEMPORAL_INLINE_INPUT_FILE_BYTES = 64 * 1024;
16
+ export const MAX_SUBMITTED_INLINE_INPUT_FILE_BYTES = 64 * 1024;
17
17
 
18
18
  /**
19
19
  * Max time the run route will block waiting for a play to finish before
@@ -0,0 +1,81 @@
1
+ type FetchLike = typeof fetch;
2
+
3
+ const vercelProtectionCookieCache = new Map<string, Promise<string | null>>();
4
+
5
+ export function addVercelProtectionBypassSearchParams(
6
+ url: URL,
7
+ token: string | null | undefined,
8
+ ): void {
9
+ const normalized = token?.trim();
10
+ if (!normalized) return;
11
+ url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
12
+ url.searchParams.set('x-vercel-protection-bypass', normalized);
13
+ }
14
+
15
+ export function vercelProtectionBypassHeader(
16
+ token: string | null | undefined,
17
+ ): Record<string, string> {
18
+ const normalized = token?.trim();
19
+ return normalized ? { 'x-vercel-protection-bypass': normalized } : {};
20
+ }
21
+
22
+ function setCookieHeaders(headers: Headers): string[] {
23
+ const getter = (headers as Headers & { getSetCookie?: () => string[] })
24
+ .getSetCookie;
25
+ if (typeof getter === 'function') {
26
+ return getter.call(headers);
27
+ }
28
+ const single = headers.get('set-cookie');
29
+ return single ? [single] : [];
30
+ }
31
+
32
+ export function cookieHeaderFromSetCookie(headers: Headers): string | null {
33
+ const cookies = setCookieHeaders(headers)
34
+ .flatMap((header) => header.split(/,(?=\s*[^;,=]+=[^;,]+)/g))
35
+ .map((header) => header.split(';', 1)[0]?.trim() ?? '')
36
+ .filter(Boolean);
37
+ return cookies.length > 0 ? cookies.join('; ') : null;
38
+ }
39
+
40
+ export async function resolveVercelProtectionBypassCookie(input: {
41
+ baseUrl: string | null | undefined;
42
+ token: string | null | undefined;
43
+ fetchImpl?: FetchLike;
44
+ }): Promise<string | null> {
45
+ const token = input.token?.trim();
46
+ const baseUrl = input.baseUrl?.trim().replace(/\/$/, '');
47
+ if (!token || !baseUrl) return null;
48
+
49
+ const cacheKey = `${baseUrl}\n${token}`;
50
+ const cached = vercelProtectionCookieCache.get(cacheKey);
51
+ if (cached) return await cached;
52
+
53
+ const promise = (async () => {
54
+ const url = new URL(`${baseUrl}/api/v2/health`);
55
+ addVercelProtectionBypassSearchParams(url, token);
56
+ const response = await (input.fetchImpl ?? fetch)(url.toString(), {
57
+ headers: { 'x-vercel-protection-bypass': token },
58
+ }).catch(() => null);
59
+ return response ? cookieHeaderFromSetCookie(response.headers) : null;
60
+ })();
61
+ vercelProtectionCookieCache.set(cacheKey, promise);
62
+ return await promise;
63
+ }
64
+
65
+ export async function vercelProtectionBypassHeaders(input: {
66
+ baseUrl: string | null | undefined;
67
+ token: string | null | undefined;
68
+ fetchImpl?: FetchLike;
69
+ }): Promise<Record<string, string>> {
70
+ const token = input.token?.trim();
71
+ if (!token) return {};
72
+ const cookie = await resolveVercelProtectionBypassCookie({
73
+ baseUrl: input.baseUrl,
74
+ token,
75
+ fetchImpl: input.fetchImpl,
76
+ });
77
+ return {
78
+ ...vercelProtectionBypassHeader(token),
79
+ ...(cookie ? { cookie } : {}),
80
+ };
81
+ }
@@ -1,4 +1,5 @@
1
1
  export type WorkReceiptStateStatus =
2
+ | 'queued'
2
3
  | 'pending'
3
4
  | 'running'
4
5
  | 'completed'
@@ -51,6 +52,12 @@ export type WorkReceiptInvalidLeaseReason = Extract<
51
52
  >['reason'];
52
53
 
53
54
  export type WorkReceiptState =
55
+ | {
56
+ kind: 'queued';
57
+ terminal: false;
58
+ reusable: false;
59
+ lease: WorkReceiptLeaseState;
60
+ }
54
61
  | {
55
62
  kind: 'pending';
56
63
  terminal: false;
@@ -209,7 +216,8 @@ export function classifyWorkReceiptState(
209
216
  const lease = classifyWorkReceiptLeaseState(receipt, nowMs);
210
217
  switch (receipt.status) {
211
218
  case 'pending':
212
- return { kind: 'pending', terminal: false, reusable: false, lease };
219
+ case 'queued':
220
+ return { kind: 'queued', terminal: false, reusable: false, lease };
213
221
  case 'running':
214
222
  return { kind: 'running', terminal: false, reusable: false, lease };
215
223
  case 'completed':
@@ -255,7 +263,11 @@ export function decideWorkReceiptClaim(input: {
255
263
  }
256
264
  return { kind: 'blocked_failed' };
257
265
  }
258
- if (state.kind === 'pending' || state.kind === 'running') {
266
+ if (
267
+ state.kind === 'pending' ||
268
+ state.kind === 'queued' ||
269
+ state.kind === 'running'
270
+ ) {
259
271
  if (state.lease.kind === 'active') {
260
272
  if (
261
273
  (state.kind !== 'running' ||
@@ -284,7 +296,7 @@ export function decideWorkReceiptClaim(input: {
284
296
  if (input.forceRefresh === true && input.reclaimRunning === true) {
285
297
  return { kind: 'claim' };
286
298
  }
287
- if (state.kind === 'pending') {
299
+ if (state.kind === 'pending' || state.kind === 'queued') {
288
300
  return { kind: 'claim' };
289
301
  }
290
302
  if (
@@ -364,6 +376,32 @@ function baseRunningOwnerDecision(
364
376
  return { kind: 'allow' };
365
377
  }
366
378
 
379
+ function baseActiveOwnerDecision(
380
+ input: WorkReceiptOwnerOperationInput,
381
+ ): WorkReceiptOwnerOperationDecision {
382
+ if (!input.receipt) {
383
+ return { kind: 'reject', reason: 'missing_receipt' };
384
+ }
385
+ if (
386
+ input.receipt.status !== 'queued' &&
387
+ input.receipt.status !== 'pending' &&
388
+ input.receipt.status !== 'running'
389
+ ) {
390
+ return { kind: 'reject', reason: 'not_running' };
391
+ }
392
+ const ownerRunId = normalize(input.ownerRunId);
393
+ if (!ownerRunId || ownerRunIdForReceipt(input.receipt) !== ownerRunId) {
394
+ return { kind: 'reject', reason: 'owner_mismatch' };
395
+ }
396
+ if (
397
+ normalizeAttempt(input.receipt.leaseOwnerAttempt) !==
398
+ normalizeAttempt(input.ownerRunAttempt)
399
+ ) {
400
+ return { kind: 'reject', reason: 'attempt_mismatch' };
401
+ }
402
+ return { kind: 'allow' };
403
+ }
404
+
367
405
  /**
368
406
  * Law for complete/fail/skip. A terminal write may only be made by the
369
407
  * running receipt owner for the same attempt. When the receipt has a lease id,
@@ -385,14 +423,21 @@ export function decideWorkReceiptTerminalWrite(
385
423
  }
386
424
 
387
425
  /**
388
- * Law for a single suspended receipt release. It uses the same ownership fence
389
- * as terminal writes: only the running owner can release, and a leased receipt
390
- * requires the exact lease id.
426
+ * Law for a single suspended receipt release. The owner may release queued or
427
+ * running work, and a leased receipt requires the exact lease id.
391
428
  */
392
429
  export function decideWorkReceiptExactRelease(
393
430
  input: WorkReceiptOwnerOperationInput,
394
431
  ): WorkReceiptOwnerOperationDecision {
395
- return decideWorkReceiptTerminalWrite(input);
432
+ const ownerDecision = baseActiveOwnerDecision(input);
433
+ if (ownerDecision.kind === 'reject') return ownerDecision;
434
+
435
+ const receiptLeaseId = normalize(input.receipt?.leaseId);
436
+ if (!receiptLeaseId) return { kind: 'allow' };
437
+ if (receiptLeaseId !== normalize(input.leaseId)) {
438
+ return { kind: 'reject', reason: 'lease_mismatch' };
439
+ }
440
+ return { kind: 'allow' };
396
441
  }
397
442
 
398
443
  /**
@@ -403,7 +448,7 @@ export function decideWorkReceiptExactRelease(
403
448
  export function decideWorkReceiptOwnerRelease(
404
449
  input: WorkReceiptOwnerOperationInput,
405
450
  ): WorkReceiptOwnerOperationDecision {
406
- return baseRunningOwnerDecision(input);
451
+ return baseActiveOwnerDecision(input);
407
452
  }
408
453
 
409
454
  /**
@@ -413,7 +458,7 @@ export function decideWorkReceiptOwnerRelease(
413
458
  export function decideWorkReceiptHeartbeat(
414
459
  input: WorkReceiptOwnerOperationInput,
415
460
  ): WorkReceiptOwnerOperationDecision {
416
- const ownerDecision = baseRunningOwnerDecision(input);
461
+ const ownerDecision = baseActiveOwnerDecision(input);
417
462
  if (ownerDecision.kind === 'reject') return ownerDecision;
418
463
 
419
464
  const requestLeaseId = normalize(input.leaseId);
@@ -25,6 +25,7 @@ export function workReceiptFailureKindCode(
25
25
  }
26
26
 
27
27
  export type WorkReceiptStatus =
28
+ | 'queued'
28
29
  | 'pending'
29
30
  | 'running'
30
31
  | 'completed'
@@ -102,6 +103,17 @@ export type WorkReceiptBatchHeartbeatCommand = {
102
103
  leaseTtlMs?: number | null;
103
104
  };
104
105
 
106
+ export type WorkReceiptBatchMarkRunningCommand = {
107
+ playName: string;
108
+ receipts: Array<{
109
+ runId: string;
110
+ key: string;
111
+ leaseId?: string | null;
112
+ runAttempt?: number | null;
113
+ }>;
114
+ leaseTtlMs?: number | null;
115
+ };
116
+
105
117
  export type WorkReceiptGetCommand = {
106
118
  playName: string;
107
119
  key: string;
@@ -111,6 +123,7 @@ export type WorkReceiptClaim =
111
123
  | {
112
124
  disposition: 'claimed';
113
125
  receipt: WorkReceipt;
126
+ wasFailed?: boolean;
114
127
  }
115
128
  | {
116
129
  disposition: 'reused';
@@ -135,6 +148,9 @@ export type WorkReceiptStore = {
135
148
  claimReceipts?(
136
149
  input: WorkReceiptBatchClaimCommand,
137
150
  ): Promise<WorkReceiptClaim[]>;
151
+ markReceiptsRunning?(
152
+ input: WorkReceiptBatchMarkRunningCommand,
153
+ ): Promise<Array<WorkReceipt | null>>;
138
154
  completeReceipt(
139
155
  input: WorkReceiptCommand & { output: unknown },
140
156
  ): Promise<WorkReceipt | null>;
@@ -150,9 +166,7 @@ export type WorkReceiptStore = {
150
166
  failReceipts?(
151
167
  input: WorkReceiptBatchFailCommand,
152
168
  ): Promise<Array<WorkReceipt | null>>;
153
- releaseReceipt?(
154
- input: WorkReceiptCommand,
155
- ): Promise<WorkReceipt | null>;
169
+ releaseReceipt?(input: WorkReceiptCommand): Promise<WorkReceipt | null>;
156
170
  heartbeatReceipts?(
157
171
  input: WorkReceiptBatchHeartbeatCommand,
158
172
  ): Promise<Array<WorkReceipt | null>>;
@@ -41,10 +41,6 @@ const DAYTONA_COMPUTE_PRICING_USD = {
41
41
  includedStorageGiB: 5,
42
42
  } as const;
43
43
 
44
- const TEMPORAL_COMPUTE_PRICING_USD = {
45
- action: 50 / 1_000_000,
46
- } as const;
47
-
48
44
  const NEON_COMPUTE_PRICING_USD = {
49
45
  cuHour: 0.106,
50
46
  defaultCu: 1,
@@ -97,22 +93,6 @@ export function resolveDaytonaSandboxComputeItem(input: {
97
93
  };
98
94
  }
99
95
 
100
- export function resolveTemporalComputeItem(input: {
101
- itemId: string;
102
- actions: number;
103
- metadata?: unknown;
104
- }): ComputeBillingItem {
105
- const actions = Math.max(0, Math.ceil(input.actions));
106
- return {
107
- itemId: input.itemId,
108
- source: 'temporal',
109
- unit: 'action',
110
- units: actions,
111
- providerCostUsd: roundUsd(actions * TEMPORAL_COMPUTE_PRICING_USD.action),
112
- metadata: input.metadata,
113
- };
114
- }
115
-
116
96
  export function resolveNeonComputeItem(input: {
117
97
  itemId: string;
118
98
  activeSeconds: number;
@@ -3,6 +3,7 @@ import type { PlayExecutionFileRef } from './file-refs';
3
3
  const PLAY_DATASET_BRAND = Symbol.for('deepline.play.dataset');
4
4
  const NODE_INSPECT_CUSTOM = Symbol.for('nodejs.util.inspect.custom');
5
5
  const DEFAULT_MATERIALIZE_LIMIT = 10_000;
6
+ export const PLAY_DATASET_EXECUTION_PAGE_ROWS = 1_000;
6
7
 
7
8
  export type PlayDatasetKind = 'csv' | 'map';
8
9
 
@@ -538,19 +539,119 @@ export function createPlayDataset<T>(
538
539
  });
539
540
  }
540
541
 
542
+ export type MaterializePlayDatasetInputOptions<T> = {
543
+ onRow?: (row: T, index: number) => void;
544
+ };
545
+
546
+ export type IteratePlayDatasetInputPagesOptions<T> = {
547
+ pageSize?: number;
548
+ onRow?: (row: T, index: number) => void;
549
+ };
550
+
551
+ function normalizeDatasetExecutionPageSize(value: number | undefined): number {
552
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
553
+ return PLAY_DATASET_EXECUTION_PAGE_ROWS;
554
+ }
555
+ return Math.max(1, Math.floor(value));
556
+ }
557
+
558
+ export async function* iteratePlayDatasetInputPages<T>(
559
+ input: PlayDatasetInput<T>,
560
+ options?: IteratePlayDatasetInputPagesOptions<T>,
561
+ ): AsyncIterable<{ rows: T[]; offset: number }> {
562
+ const pageSize = normalizeDatasetExecutionPageSize(options?.pageSize);
563
+ let page: T[] = [];
564
+ let offset = 0;
565
+ let index = 0;
566
+ const pushRow = (row: T): { rows: T[]; offset: number } | null => {
567
+ options?.onRow?.(row, index);
568
+ index += 1;
569
+ page.push(row);
570
+ if (page.length < pageSize) {
571
+ return null;
572
+ }
573
+ const fullPage = page;
574
+ const fullOffset = offset;
575
+ page = [];
576
+ offset += fullPage.length;
577
+ return { rows: fullPage, offset: fullOffset };
578
+ };
579
+
580
+ const flush = (): { rows: T[]; offset: number } | null => {
581
+ if (page.length === 0) return null;
582
+ const finalPage = page;
583
+ const finalOffset = offset;
584
+ page = [];
585
+ offset += finalPage.length;
586
+ return { rows: finalPage, offset: finalOffset };
587
+ };
588
+
589
+ const emitRow = async function* (row: T) {
590
+ const ready = pushRow(row);
591
+ if (ready) yield ready;
592
+ };
593
+
594
+ if (isPlayDataset(input)) {
595
+ for await (const row of input) {
596
+ yield* emitRow(row);
597
+ }
598
+ const finalPage = flush();
599
+ if (finalPage) yield finalPage;
600
+ return;
601
+ }
602
+
603
+ if (Array.isArray(input)) {
604
+ for (const row of input) {
605
+ yield* emitRow(row);
606
+ }
607
+ const finalPage = flush();
608
+ if (finalPage) yield finalPage;
609
+ return;
610
+ }
611
+
612
+ if (
613
+ input != null &&
614
+ typeof input === 'object' &&
615
+ Symbol.asyncIterator in input
616
+ ) {
617
+ for await (const row of input as AsyncIterable<T>) {
618
+ yield* emitRow(row);
619
+ }
620
+ const finalPage = flush();
621
+ if (finalPage) yield finalPage;
622
+ return;
623
+ }
624
+
625
+ for (const row of input as Iterable<T>) {
626
+ yield* emitRow(row);
627
+ }
628
+ const finalPage = flush();
629
+ if (finalPage) yield finalPage;
630
+ }
631
+
541
632
  export async function materializePlayDatasetInput<T>(
542
633
  input: PlayDatasetInput<T>,
634
+ options?: MaterializePlayDatasetInputOptions<T>,
543
635
  ): Promise<T[]> {
636
+ const pushRow = (rows: T[], row: T): void => {
637
+ options?.onRow?.(row, rows.length);
638
+ rows.push(row);
639
+ };
640
+
544
641
  if (isPlayDataset(input)) {
545
642
  const rows: T[] = [];
546
643
  for await (const row of input) {
547
- rows.push(row);
644
+ pushRow(rows, row);
548
645
  }
549
646
  return rows;
550
647
  }
551
648
 
552
649
  if (Array.isArray(input)) {
553
- return [...input];
650
+ const rows: T[] = [];
651
+ for (const row of input) {
652
+ pushRow(rows, row);
653
+ }
654
+ return rows;
554
655
  }
555
656
 
556
657
  const rows: T[] = [];
@@ -560,13 +661,13 @@ export async function materializePlayDatasetInput<T>(
560
661
  Symbol.asyncIterator in input
561
662
  ) {
562
663
  for await (const row of input as AsyncIterable<T>) {
563
- rows.push(row);
664
+ pushRow(rows, row);
564
665
  }
565
666
  return rows;
566
667
  }
567
668
 
568
669
  for (const row of input as Iterable<T>) {
569
- rows.push(row);
670
+ pushRow(rows, row);
570
671
  }
571
672
  return rows;
572
673
  }