deepline 0.1.182 → 0.1.183

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 (66) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
  2. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
  3. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +633 -107
  4. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
  5. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1747 -1890
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
  7. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
  8. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
  9. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
  10. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
  11. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
  12. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
  13. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
  14. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
  15. package/dist/bundling-sources/sdk/src/client.ts +41 -0
  16. package/dist/bundling-sources/sdk/src/http.ts +23 -8
  17. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
  18. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  19. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +132 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1605 -281
  26. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
  32. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
  36. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
  40. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
  42. package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
  43. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
  44. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2037 -262
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
  51. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
  52. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
  53. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
  58. package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
  59. package/dist/cli/index.js +76 -17
  60. package/dist/cli/index.mjs +76 -17
  61. package/dist/index.d.mts +2 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +63 -9
  64. package/dist/index.mjs +63 -9
  65. package/dist/plays/bundle-play-file.mjs +2 -2
  66. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ export const PLAY_RUNTIME_CONTRACT = 2;
2
+ export const PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED = 1;
3
+ export const PLAY_RUNTIME_CONTRACT_HEADER = 'x-deepline-runtime-contract';
4
+
5
+ export const RUNTIME_CONTRACT_CHANGELOG = [
6
+ {
7
+ contract: 2,
8
+ changed:
9
+ 'Runtime receipt batch responses use positional nulls and v2 lease-aware receipt semantics.',
10
+ compatOwner: 'runtime',
11
+ compatRemoval: 'ledger cutover milestone M1',
12
+ },
13
+ ] as const;
14
+
15
+ export type RuntimeContractResolution =
16
+ | { ok: true; contract: number }
17
+ | {
18
+ ok: false;
19
+ requestedContract: number;
20
+ currentContract: number;
21
+ minSupportedContract: number;
22
+ };
23
+
24
+ export function resolvePlayRuntimeContract(
25
+ rawContract: string | null | undefined,
26
+ ): RuntimeContractResolution {
27
+ const trimmed = rawContract?.trim();
28
+ const requestedContract = trimmed ? Number(trimmed) : 1;
29
+ if (
30
+ !Number.isInteger(requestedContract) ||
31
+ requestedContract < PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED ||
32
+ requestedContract > PLAY_RUNTIME_CONTRACT
33
+ ) {
34
+ return {
35
+ ok: false,
36
+ requestedContract,
37
+ currentContract: PLAY_RUNTIME_CONTRACT,
38
+ minSupportedContract: PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED,
39
+ };
40
+ }
41
+ return { ok: true, contract: requestedContract };
42
+ }
@@ -0,0 +1,183 @@
1
+ export type RuntimeSheetRowStatus =
2
+ | 'pending'
3
+ | 'running'
4
+ | 'failed'
5
+ | 'enriched'
6
+ | 'stale';
7
+
8
+ export type RuntimeSheetAttemptIdentity = {
9
+ attemptId: string | null;
10
+ attemptOwnerRunId: string | null;
11
+ attemptExpiresAt: string | null;
12
+ attemptSeq?: number | null;
13
+ };
14
+
15
+ export type RuntimeSheetRowAttemptInput = RuntimeSheetAttemptIdentity & {
16
+ status: RuntimeSheetRowStatus;
17
+ runId: string | null;
18
+ };
19
+
20
+ export type RuntimeSheetAttemptRelation =
21
+ | { kind: 'unowned' }
22
+ | { kind: 'same_attempt' }
23
+ | { kind: 'expired' }
24
+ | { kind: 'same_owner_newer_attempt' }
25
+ | { kind: 'same_owner_stale_attempt' }
26
+ | { kind: 'other_active_owner' }
27
+ | {
28
+ kind: 'malformed';
29
+ reason: 'invalid_row_expiry';
30
+ };
31
+
32
+ export type RuntimeSheetPrepareDecision =
33
+ | { kind: 'claim_executable' }
34
+ | { kind: 'reuse_completed' }
35
+ | { kind: 'blocked_by_active_owner' };
36
+
37
+ export type RuntimeSheetTerminalDecision =
38
+ | { kind: 'write_terminal' }
39
+ | { kind: 'reject_stale_attempt' };
40
+
41
+ export type RuntimeSheetReleaseDecision =
42
+ | { kind: 'release' }
43
+ | { kind: 'skip' };
44
+
45
+ function normalize(value: string | null | undefined): string | null {
46
+ const trimmed = value?.trim();
47
+ return trimmed ? trimmed : null;
48
+ }
49
+
50
+ function parseOptionalMs(value: string | null): number | null {
51
+ if (!value) return null;
52
+ const parsed = Date.parse(value);
53
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
54
+ }
55
+
56
+ function normalizeAttemptSeq(value: number | null | undefined): number {
57
+ return Number.isFinite(value) ? Math.trunc(value as number) : 0;
58
+ }
59
+
60
+ export function classifyRuntimeSheetAttemptRelation(input: {
61
+ row: RuntimeSheetRowAttemptInput;
62
+ request: RuntimeSheetAttemptIdentity;
63
+ nowMs?: number;
64
+ }): RuntimeSheetAttemptRelation {
65
+ const nowMs = input.nowMs ?? Date.now();
66
+ const rowAttemptId = normalize(input.row.attemptId);
67
+ const requestAttemptId = normalize(input.request.attemptId);
68
+ if (!rowAttemptId) return { kind: 'unowned' };
69
+ if (rowAttemptId === requestAttemptId) return { kind: 'same_attempt' };
70
+
71
+ const rowExpiresAt = normalize(input.row.attemptExpiresAt);
72
+ const rowExpiresAtMs = parseOptionalMs(rowExpiresAt);
73
+ if (rowExpiresAtMs === null) {
74
+ return { kind: 'malformed', reason: 'invalid_row_expiry' };
75
+ }
76
+ if (Number.isNaN(rowExpiresAtMs)) {
77
+ return { kind: 'malformed', reason: 'invalid_row_expiry' };
78
+ }
79
+ if (rowExpiresAtMs <= nowMs) return { kind: 'expired' };
80
+
81
+ const rowOwner =
82
+ normalize(input.row.attemptOwnerRunId) ?? normalize(input.row.runId);
83
+ const requestOwner = normalize(input.request.attemptOwnerRunId);
84
+ const rowAttemptSeq = normalizeAttemptSeq(input.row.attemptSeq);
85
+ const requestAttemptSeq = normalizeAttemptSeq(input.request.attemptSeq);
86
+ if (rowOwner && requestOwner && rowOwner === requestOwner) {
87
+ return requestAttemptSeq > rowAttemptSeq
88
+ ? { kind: 'same_owner_newer_attempt' }
89
+ : { kind: 'same_owner_stale_attempt' };
90
+ }
91
+
92
+ return { kind: 'other_active_owner' };
93
+ }
94
+
95
+ export function decideRuntimeSheetPrepare(input: {
96
+ row: RuntimeSheetRowAttemptInput;
97
+ request: RuntimeSheetAttemptIdentity;
98
+ missingRequiredOutput?: boolean;
99
+ nowMs?: number;
100
+ }): RuntimeSheetPrepareDecision {
101
+ if (input.row.status === 'stale' || input.row.status === 'failed') {
102
+ return { kind: 'claim_executable' };
103
+ }
104
+ const relation = classifyRuntimeSheetAttemptRelation(input);
105
+ if (input.row.status === 'pending' || input.row.status === 'running') {
106
+ return relation.kind === 'other_active_owner' || relation.kind === 'malformed'
107
+ ? { kind: 'blocked_by_active_owner' }
108
+ : { kind: 'claim_executable' };
109
+ }
110
+ if (input.row.status === 'enriched') {
111
+ if (!input.missingRequiredOutput) return { kind: 'reuse_completed' };
112
+ return relation.kind === 'same_owner_stale_attempt' ||
113
+ relation.kind === 'malformed'
114
+ ? { kind: 'blocked_by_active_owner' }
115
+ : { kind: 'claim_executable' };
116
+ }
117
+ return assertNever(input.row.status);
118
+ }
119
+
120
+ export function decideRuntimeSheetTerminalWrite(input: {
121
+ row: RuntimeSheetRowAttemptInput;
122
+ request: RuntimeSheetAttemptIdentity;
123
+ nowMs?: number;
124
+ }): RuntimeSheetTerminalDecision {
125
+ const relation = classifyRuntimeSheetAttemptRelation(input);
126
+ if (
127
+ relation.kind === 'unowned' ||
128
+ relation.kind === 'same_attempt' ||
129
+ relation.kind === 'expired' ||
130
+ relation.kind === 'same_owner_newer_attempt'
131
+ ) {
132
+ return { kind: 'write_terminal' };
133
+ }
134
+ if (input.row.status === 'enriched') {
135
+ const rowAttemptSeq = normalizeAttemptSeq(input.row.attemptSeq);
136
+ const requestAttemptSeq = normalizeAttemptSeq(input.request.attemptSeq);
137
+ if (requestAttemptSeq > rowAttemptSeq) {
138
+ return { kind: 'write_terminal' };
139
+ }
140
+ }
141
+ return { kind: 'reject_stale_attempt' };
142
+ }
143
+
144
+ /**
145
+ * Decide whether a controlled run-fatal teardown may release (clear the attempt
146
+ * lease on) a row. Only rows the releasing attempt currently owns are cleared:
147
+ * the row must carry an attempt lease whose owner run id and attempt generation
148
+ * (seq) match the releasing run. When the request carries a concrete attempt id
149
+ * the row's attempt id must match it too; when the request attempt id is null,
150
+ * every attempt id the run holds at that generation is releasable (the workers
151
+ * runner mints a distinct attempt id per map chunk under one owner + seq). Row
152
+ * status is irrelevant — the lease strands work receipts and blocks the write
153
+ * fence whether the row is pending, running, failed, or enriched — but an
154
+ * unowned row is never touched.
155
+ */
156
+ export function decideRuntimeSheetRelease(input: {
157
+ row: RuntimeSheetRowAttemptInput;
158
+ request: RuntimeSheetAttemptIdentity;
159
+ }): RuntimeSheetReleaseDecision {
160
+ const rowAttemptId = normalize(input.row.attemptId);
161
+ if (!rowAttemptId) return { kind: 'skip' };
162
+ const rowOwner =
163
+ normalize(input.row.attemptOwnerRunId) ?? normalize(input.row.runId);
164
+ const requestOwner = normalize(input.request.attemptOwnerRunId);
165
+ if (!rowOwner || !requestOwner || rowOwner !== requestOwner) {
166
+ return { kind: 'skip' };
167
+ }
168
+ if (
169
+ normalizeAttemptSeq(input.row.attemptSeq) !==
170
+ normalizeAttemptSeq(input.request.attemptSeq)
171
+ ) {
172
+ return { kind: 'skip' };
173
+ }
174
+ const requestAttemptId = normalize(input.request.attemptId);
175
+ if (requestAttemptId && requestAttemptId !== rowAttemptId) {
176
+ return { kind: 'skip' };
177
+ }
178
+ return { kind: 'release' };
179
+ }
180
+
181
+ function assertNever(value: never): never {
182
+ throw new Error(`Unhandled runtime sheet row status: ${String(value)}`);
183
+ }
@@ -0,0 +1,88 @@
1
+ import { resolveMapRowOutcomeKey } from './map-row-outcome';
2
+
3
+ export interface RuntimeSheetBlockedRowDetail {
4
+ rowKey: string | null;
5
+ attemptId: string | null;
6
+ attemptOwnerRunId: string | null;
7
+ attemptExpiresAt: string | null;
8
+ }
9
+
10
+ function stringField(
11
+ row: Record<string, unknown>,
12
+ ...fieldNames: string[]
13
+ ): string | null {
14
+ for (const fieldName of fieldNames) {
15
+ const value = row[fieldName];
16
+ if (typeof value === 'string' && value.trim()) {
17
+ return value;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function runtimeSheetBlockedRowDetail(
24
+ row: Record<string, unknown>,
25
+ ): RuntimeSheetBlockedRowDetail {
26
+ return {
27
+ rowKey: resolveMapRowOutcomeKey(row),
28
+ attemptId: stringField(
29
+ row,
30
+ 'attemptId',
31
+ '_attempt_id',
32
+ '__deeplineAttemptId',
33
+ ),
34
+ attemptOwnerRunId: stringField(
35
+ row,
36
+ 'attemptOwnerRunId',
37
+ '_attempt_owner_run_id',
38
+ '__deeplineAttemptOwnerRunId',
39
+ ),
40
+ attemptExpiresAt: stringField(
41
+ row,
42
+ 'attemptExpiresAt',
43
+ '_attempt_expires_at',
44
+ '__deeplineAttemptExpiresAt',
45
+ ),
46
+ };
47
+ }
48
+
49
+ export class RuntimeSheetRowsBlockedError extends Error {
50
+ readonly tableNamespace: string;
51
+ readonly rowKeys: string[];
52
+ readonly owningAttempts: string[];
53
+ readonly blockedRowDetails: RuntimeSheetBlockedRowDetail[];
54
+
55
+ constructor(input: {
56
+ tableNamespace: string;
57
+ blockedRows: Record<string, unknown>[];
58
+ }) {
59
+ const blockedRowDetails = input.blockedRows.map(
60
+ runtimeSheetBlockedRowDetail,
61
+ );
62
+ const rowKeys = blockedRowDetails
63
+ .map((detail) => detail.rowKey)
64
+ .filter((key): key is string => Boolean(key))
65
+ .slice(0, 20);
66
+ const owningAttempts = blockedRowDetails
67
+ .map((detail) => detail.attemptId)
68
+ .filter((attempt): attempt is string => Boolean(attempt));
69
+ const uniqueOwningAttempts = Array.from(new Set(owningAttempts)).slice(
70
+ 0,
71
+ 20,
72
+ );
73
+ super(
74
+ `ctx.dataset("${input.tableNamespace}") is blocked by ${input.blockedRows.length} active runtime sheet row lease(s) owned by another attempt. Retry after the owning attempt finishes or the row leases expire.` +
75
+ (rowKeys.length > 0
76
+ ? ` Blocked row keys: ${rowKeys.join(', ')}.`
77
+ : '') +
78
+ (uniqueOwningAttempts.length > 0
79
+ ? ` Owning attempts: ${uniqueOwningAttempts.join(', ')}.`
80
+ : ''),
81
+ );
82
+ this.name = 'RuntimeSheetRowsBlockedError';
83
+ this.tableNamespace = input.tableNamespace;
84
+ this.rowKeys = rowKeys;
85
+ this.owningAttempts = uniqueOwningAttempts;
86
+ this.blockedRowDetails = blockedRowDetails.slice(0, 20);
87
+ }
88
+ }
@@ -19,6 +19,7 @@ import type { ExecutionPlan } from './execution-plan';
19
19
  import type { PlayRuntimeManifestMap } from '../plays/compiler-manifest';
20
20
  import type { PreloadedRuntimeDbSession } from './db-session';
21
21
  import type { PlayRunnerRuntimeTiming } from './protocol';
22
+ import type { RuntimeTestPolicyOverrides } from './test-runtime-seams';
22
23
 
23
24
  export const PLAY_SCHEDULER_BACKENDS = {
24
25
  temporal: 'temporal',
@@ -83,7 +84,9 @@ export type PlaySchedulerSubmitInput = {
83
84
  artifactHash: string;
84
85
  graphHash: string;
85
86
  input: Record<string, unknown>;
86
- /** Run-level --force cache bypass for dataset rows and ctx.tools.execute receipts. */
87
+ /** Start a fresh run graph and recompute runtime-sheet rows. */
88
+ force?: boolean;
89
+ /** Explicit cache bypass for completed ctx.tools.execute receipts. */
87
90
  forceToolRefresh?: boolean | null;
88
91
  inputFile?: {
89
92
  name?: string;
@@ -149,6 +152,10 @@ export type PlaySchedulerSubmitInput = {
149
152
  runtimeDeployVersion?: string | null;
150
153
  /** Request-scoped Vercel Deployment Protection bypass for preview runtime callbacks. */
151
154
  vercelProtectionBypassToken?: string | null;
155
+ /** Request-scoped, dev-only runtime fault injection header for black-box durability tests. */
156
+ runtimeTestFaultHeader?: string | null;
157
+ /** Request-scoped, dev-only runtime policy overrides for black-box durability tests. */
158
+ testPolicyOverrides?: RuntimeTestPolicyOverrides | null;
152
159
  /** Millisecond epoch timestamp captured immediately before scheduler submit. */
153
160
  submittedAtMs?: number;
154
161
  /** Scheduler backend that owns orchestration for this run. */
@@ -0,0 +1,103 @@
1
+ export function activeRuntimeSheetAttemptFenceSql(
2
+ tableAlias: string,
3
+ attemptIdExpression: string,
4
+ attemptOwnerRunIdExpression: string,
5
+ attemptExpiresAtExpression: string,
6
+ attemptSeqExpression?: string,
7
+ ): string {
8
+ const activeSameOwnerNewerAttempt =
9
+ attemptSeqExpression === undefined
10
+ ? `${tableAlias}._attempt_expires_at < ${attemptExpiresAtExpression}`
11
+ // Same owner + same sequence is a retry of the same logical run attempt.
12
+ // It may reclaim non-terminal rows; terminal rows are still guarded by
13
+ // newerTerminalRuntimeSheetRowSql before this predicate is considered.
14
+ : `COALESCE(${tableAlias}._attempt_seq, 0) <= ${attemptSeqExpression}::integer`;
15
+ return `(
16
+ /* owner: runtime; remove null-attempt compat after ledger cutover M1 */
17
+ ${tableAlias}._attempt_id IS NULL
18
+ OR ${tableAlias}._attempt_id = ${attemptIdExpression}
19
+ OR ${tableAlias}._attempt_expires_at <= now()
20
+ OR (
21
+ coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
22
+ AND ${activeSameOwnerNewerAttempt}
23
+ )
24
+ )`;
25
+ }
26
+
27
+ /**
28
+ * Fence for a controlled attempt-lease RELEASE (run-fatal teardown). Matches
29
+ * only rows the releasing attempt currently owns: the row must carry an attempt
30
+ * lease whose owner run id and attempt generation match the releasing run. When
31
+ * `attemptIdExpression` is a non-null param it further pins the exact attempt id
32
+ * (single-attempt callers like the in-process app runner); when it is NULL the
33
+ * predicate releases every attempt id the run holds at that generation (the
34
+ * workers runner mints a distinct attempt id per map chunk under one owner run
35
+ * id + seq). It never matches a row owned by a different run, so a release can
36
+ * never weaken another live attempt's lease.
37
+ */
38
+ export function releasableRuntimeSheetAttemptFenceSql(
39
+ tableAlias: string,
40
+ attemptIdExpression: string,
41
+ attemptOwnerRunIdExpression: string,
42
+ attemptSeqExpression: string,
43
+ ): string {
44
+ return `(
45
+ ${tableAlias}._attempt_id IS NOT NULL
46
+ AND coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
47
+ AND COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
48
+ AND (${attemptIdExpression} IS NULL OR ${tableAlias}._attempt_id = ${attemptIdExpression})
49
+ )`;
50
+ }
51
+
52
+ export function sameOwnerTerminalAttemptEpochSql(
53
+ tableAlias: string,
54
+ attemptOwnerRunIdExpression: string,
55
+ attemptSeqExpression: string,
56
+ ): string {
57
+ return `(
58
+ ${tableAlias}._status = 'enriched'
59
+ AND coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) = ${attemptOwnerRunIdExpression}
60
+ AND COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
61
+ )`;
62
+ }
63
+
64
+ export function newerTerminalRuntimeSheetRowSql(
65
+ tableAlias: string,
66
+ attemptExpiresAtExpression: string,
67
+ attemptSeqExpression?: string,
68
+ attemptIdExpression?: string,
69
+ attemptOwnerRunIdExpression?: string,
70
+ ): string {
71
+ if (attemptSeqExpression !== undefined) {
72
+ const sameSeqDifferentAttemptSql = attemptOwnerRunIdExpression
73
+ ? `OR (
74
+ COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
75
+ AND ${tableAlias}._attempt_id IS NOT NULL
76
+ AND ${attemptIdExpression} IS NOT NULL
77
+ AND ${tableAlias}._attempt_id IS DISTINCT FROM ${attemptIdExpression}
78
+ AND (
79
+ coalesce(${tableAlias}._attempt_owner_run_id, ${tableAlias}._run_id) IS DISTINCT FROM ${attemptOwnerRunIdExpression}
80
+ OR ${tableAlias}._attempt_expires_at > ${attemptExpiresAtExpression}
81
+ )
82
+ )`
83
+ : `OR (
84
+ COALESCE(${tableAlias}._attempt_seq, 0) = ${attemptSeqExpression}::integer
85
+ AND ${tableAlias}._attempt_id IS NOT NULL
86
+ AND ${attemptIdExpression} IS NOT NULL
87
+ AND ${tableAlias}._attempt_id IS DISTINCT FROM ${attemptIdExpression}
88
+ )`;
89
+ return `(
90
+ ${tableAlias}._status = 'enriched'
91
+ AND (
92
+ COALESCE(${tableAlias}._attempt_seq, 0) > ${attemptSeqExpression}::integer
93
+ ${attemptIdExpression ? sameSeqDifferentAttemptSql : ''}
94
+ )
95
+ )`;
96
+ }
97
+ return `(
98
+ ${tableAlias}._status = 'enriched'
99
+ AND ${tableAlias}._attempt_expires_at IS NOT NULL
100
+ AND ${attemptExpiresAtExpression} IS NOT NULL
101
+ AND ${tableAlias}._attempt_expires_at > ${attemptExpiresAtExpression}
102
+ )`;
103
+ }
@@ -17,6 +17,20 @@ export type PlayExecutionSuspension =
17
17
  eventKey: string;
18
18
  timeoutMs: number;
19
19
  }>;
20
+ }
21
+ | {
22
+ kind: 'child_play';
23
+ boundaryId: string;
24
+ childRunId: string;
25
+ childPlayName: string;
26
+ }
27
+ | {
28
+ kind: 'child_play_batch';
29
+ boundaries: Array<{
30
+ boundaryId: string;
31
+ childRunId: string;
32
+ childPlayName: string;
33
+ }>;
20
34
  };
21
35
 
22
36
  export class PlayExecutionSuspendedError extends Error {
@@ -28,7 +42,11 @@ export class PlayExecutionSuspendedError extends Error {
28
42
  ? `Play execution suspended for durable sleep (${suspension.delayMs}ms).`
29
43
  : suspension.kind === 'integration_event_batch'
30
44
  ? `Play execution suspended waiting for ${suspension.boundaries.length} integration events.`
31
- : `Play execution suspended waiting for integration event ${JSON.stringify(suspension.eventKey)}.`,
45
+ : suspension.kind === 'child_play_batch'
46
+ ? `Play execution suspended waiting for ${suspension.boundaries.length} child plays.`
47
+ : suspension.kind === 'child_play'
48
+ ? `Play execution suspended waiting for child play ${JSON.stringify(suspension.childPlayName)}.`
49
+ : `Play execution suspended waiting for integration event ${JSON.stringify(suspension.eventKey)}.`,
32
50
  );
33
51
  this.name = 'PlayExecutionSuspendedError';
34
52
  this.suspension = suspension;