deepline 0.1.228 → 0.1.230

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 (39) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +76 -1842
  2. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +0 -113
  3. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +81 -968
  4. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +1 -50
  5. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +67 -15
  7. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +84 -79
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +152 -688
  9. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +3 -10
  10. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -47
  12. package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +0 -2
  13. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +12 -200
  14. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +2 -17
  15. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +6 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +28 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-execution-scope.ts +16 -64
  18. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +0 -4
  19. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +37 -9
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +0 -36
  21. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +3 -21
  22. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +9 -1
  23. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -0
  24. package/dist/cli/index.js +45 -92
  25. package/dist/cli/index.mjs +45 -92
  26. package/dist/{compiler-manifest-BhgZ23A4.d.ts → compiler-manifest-CYcwzSOJ.d.mts} +2 -0
  27. package/dist/{compiler-manifest-BhgZ23A4.d.mts → compiler-manifest-CYcwzSOJ.d.ts} +2 -0
  28. package/dist/index.d.mts +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/plays/bundle-play-file.d.mts +2 -2
  33. package/dist/plays/bundle-play-file.d.ts +2 -2
  34. package/package.json +1 -1
  35. package/dist/bundling-sources/apps/play-runner-workers/src/child-manifest-resolver.ts +0 -108
  36. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +0 -374
  37. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +0 -203
  38. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +0 -14
  39. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +0 -121
@@ -42,17 +42,7 @@ import {
42
42
  mapRowOutcomeRuntimeFields,
43
43
  resolveMapRowOutcomeKey,
44
44
  } from './map-row-outcome';
45
- import { buildChildRunId } from './child-run-id';
46
- import type { PlayCallGovernanceSnapshot } from './scheduler-backend';
47
- import {
48
- ABSURD_RELEASE_OVERRIDE_HEADER,
49
- RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER,
50
- SYNTHETIC_RUN_HEADER,
51
- } from './coordinator-headers';
52
- import {
53
- PLAY_RUNTIME_API_COMPAT_PATH,
54
- PLAY_RUNTIME_API_CURRENT_PATH,
55
- } from './runtime-api-paths';
45
+ import { PLAY_RUNTIME_API_COMPAT_PATH } from './runtime-api-paths';
56
46
  import {
57
47
  createRootRunExecutionScope,
58
48
  deriveChildRunExecutionScope,
@@ -245,8 +235,8 @@ import {
245
235
  } from './step-program-dataset-builder';
246
236
  import { readRuntimeSheetDatasetRows } from './runtime-api';
247
237
  import {
238
+ ctxRunPlayInlineOnlyMessage,
248
239
  resolveChildExecutionStrategy,
249
- inlineChildScheduledInRowMessage,
250
240
  } from './child-execution-strategy';
251
241
 
252
242
  type ResolvedPlayExecutor = (
@@ -267,6 +257,11 @@ const rowContext = new AsyncLocalStorage<{
267
257
  tableNamespace?: string;
268
258
  rowKey?: string;
269
259
  mapScope?: MapExecutionScope;
260
+ /**
261
+ * Per-resolver inline-child fan-width guard. This stays in the async row
262
+ * scope, so it becomes collectible as soon as the resolver settles.
263
+ */
264
+ inlineChildInvocationNamespaces?: Set<string>;
270
265
  }>();
271
266
  type InlineCompositionStore = {
272
267
  context: PlayContextImpl;
@@ -307,19 +302,11 @@ const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
307
302
  // provider 502; require both the branded page and our development tunnel host.
308
303
  const DEEPLINE_DEVELOPER_TUNNEL_ORIGIN_502_PATTERN =
309
304
  /\bdeeplinedeveloper\.com\s*\|\s*502\s*:\s*bad gateway\b/i;
310
- const CHILD_PLAY_LAUNCH_WRITE_CONFLICT_RETRY_DELAYS_MS = [
311
- 50, 100, 200, 400, 800,
312
- ] as const;
313
- const CHILD_PLAY_LAUNCH_ADMISSION_RETRY_DELAYS_MS = [
314
- 250, 500, 1_000, 2_000, 4_000, 8_000, 8_000, 8_000, 8_000, 8_000, 8_000,
315
- 8_000, 8_000, 8_000, 8_000,
316
- ] as const;
317
305
  const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
318
306
  const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
319
307
  // Per-row sanity cap on distinct inline child invocations. `maxPlayCallDepth`
320
- // (governor) already bounds nesting; this bounds fan-WIDTH from a single row so
321
- // a resolver that loops `ctx.runPlay` unbounded fails loudly instead of leaking
322
- // composition namespaces. Well above any legitimate authored per-row fan-out.
308
+ // (governor) already bounds nesting; this bounds fan-WIDTH from one active row
309
+ // resolver. The dedupe set is row-local, never retained for the full run.
323
310
  const MAX_INLINE_CHILD_INVOCATIONS_PER_ROW = 512;
324
311
  // Bound the retained failure detail carried in parent aggregates so a fully
325
312
  // failing large map cannot grow the progress event without limit.
@@ -354,44 +341,6 @@ export function chunkArrayForReceiptRequest<T>(items: readonly T[]): T[][] {
354
341
  type SafeFetchModule = typeof import('@shared_libs/security/safe-fetch');
355
342
  let safeFetchModule: Promise<SafeFetchModule> | null = null;
356
343
 
357
- function sleepTransientRuntimeMs(ms: number): Promise<void> {
358
- return new Promise((resolve) => setTimeout(resolve, ms));
359
- }
360
-
361
- function isConvexWriteConflictMessage(message: string): boolean {
362
- return (
363
- message.includes('OptimisticConcurrencyControlFailure') ||
364
- /changed while this mutation was being run/i.test(message) ||
365
- /write conflict/i.test(message)
366
- );
367
- }
368
-
369
- function isChildPlayLaunchAdmissionMessage(message: string): boolean {
370
- return /too many play runs (?:are )?(?:starting|active)/i.test(message);
371
- }
372
-
373
- function childPlayLaunchRetryDelayMs(input: {
374
- status: number;
375
- message: string;
376
- attempt: number;
377
- }): number | null {
378
- if (
379
- input.status === 429 &&
380
- isChildPlayLaunchAdmissionMessage(input.message)
381
- ) {
382
- return CHILD_PLAY_LAUNCH_ADMISSION_RETRY_DELAYS_MS[input.attempt] ?? null;
383
- }
384
- if (
385
- ![409, 429, 500, 502, 503, 504].includes(input.status) ||
386
- !isConvexWriteConflictMessage(input.message)
387
- ) {
388
- return null;
389
- }
390
- return (
391
- CHILD_PLAY_LAUNCH_WRITE_CONFLICT_RETRY_DELAYS_MS[input.attempt] ?? null
392
- );
393
- }
394
-
395
344
  export async function waitForNodeRuntimeMapRowsVisible(input: {
396
345
  mapName: string;
397
346
  tableNamespace: string;
@@ -1241,13 +1190,6 @@ export class PlayContextImpl {
1241
1190
  eventKey: string;
1242
1191
  timeoutMs: number;
1243
1192
  }> = [];
1244
- private pendingChildPlayBoundaries: Array<{
1245
- boundaryId: string;
1246
- childRunId: string;
1247
- childPlayName: string;
1248
- }> = [];
1249
- private activeScheduledChildPlayCalls = 0;
1250
- private scheduledChildPlayQuiescenceWaiters: Array<() => void> = [];
1251
1193
  private processedRowCount = 0;
1252
1194
  private sleepBoundaryIndex = 0;
1253
1195
  private fetchCallIndex = 0;
@@ -1255,19 +1197,6 @@ export class PlayContextImpl {
1255
1197
  createSecretRedactionContext();
1256
1198
  private mapInvocationIndex = 0;
1257
1199
  private readonly stepCallIndexByKey = new Map<string, number>();
1258
- /**
1259
- * Registry of distinct inline child invocation namespaces seen by this
1260
- * context. Inline child composition is a function call, not a run; each
1261
- * invocation's namespace (`child:<playName>#<callKey>[@<rowScope>]`) scopes
1262
- * its receipts under the parent run. See ADR 0013.
1263
- */
1264
- private readonly inlineChildInvocationOrdinals = new Map<string, number>();
1265
- /**
1266
- * Distinct inline child invocations per row scope (top-level counts as one
1267
- * scope). Backs the per-row fan-width sanity cap so a resolver looping
1268
- * `ctx.runPlay` unbounded fails loudly instead of leaking namespaces.
1269
- */
1270
- private readonly inlineChildInvocationsPerRow = new Map<string, number>();
1271
1200
  /**
1272
1201
  * Parent-level inline-child aggregates. Maintained only by the single-writer
1273
1202
  * progress path (never per child) so concurrent fan-out cannot contend. See
@@ -1478,6 +1407,15 @@ export class PlayContextImpl {
1478
1407
  return active?.context === this ? active : null;
1479
1408
  }
1480
1409
 
1410
+ /** Enforce the scalar inline-child contract at dynamic API boundaries. */
1411
+ private assertInlineChildContract(
1412
+ reason: 'dataset_child' | 'suspending_child',
1413
+ ): void {
1414
+ const composition = this.activeInlineComposition;
1415
+ if (!composition) return;
1416
+ throw new Error(ctxRunPlayInlineOnlyMessage(composition.playName, reason));
1417
+ }
1418
+
1481
1419
  private get currentExecutionScope(): RunExecutionScope {
1482
1420
  return this.activeInlineComposition?.executionScope ?? this.executionScope;
1483
1421
  }
@@ -1499,100 +1437,9 @@ export class PlayContextImpl {
1499
1437
  return active ? active.staticPipeline : this.#options.staticPipeline;
1500
1438
  }
1501
1439
 
1502
- private shouldLaunchChildPlaysThroughScheduler(): boolean {
1503
- return (
1504
- this.#options.requireSharedRateState === true &&
1505
- this.#options.durableBoundaries === true
1506
- );
1507
- }
1508
-
1509
- private childPlayBoundaryId(childRunId: string): string {
1510
- return this.durableBoundaryId(`runPlay:${childRunId}`);
1511
- }
1512
-
1513
- private recordPendingChildPlayBoundary(input: {
1514
- boundaryId: string;
1515
- childRunId: string;
1516
- childPlayName: string;
1517
- }): void {
1518
- this.pendingChildPlayBoundaries.push(input);
1519
- }
1520
-
1521
- private beginScheduledChildPlayCall(): { release: () => void } {
1522
- this.activeScheduledChildPlayCalls += 1;
1523
- let released = false;
1524
- return {
1525
- release: () => {
1526
- if (released) return;
1527
- released = true;
1528
- this.activeScheduledChildPlayCalls = Math.max(
1529
- 0,
1530
- this.activeScheduledChildPlayCalls - 1,
1531
- );
1532
- if (this.activeScheduledChildPlayCalls !== 0) return;
1533
- for (const resolve of this.scheduledChildPlayQuiescenceWaiters.splice(
1534
- 0,
1535
- )) {
1536
- resolve();
1537
- }
1538
- },
1539
- };
1540
- }
1541
-
1542
- private async waitForScheduledChildPlayQuiescence(): Promise<void> {
1543
- if (this.activeScheduledChildPlayCalls === 0) return;
1544
- await new Promise<void>((resolve) => {
1545
- this.scheduledChildPlayQuiescenceWaiters.push(resolve);
1546
- });
1547
- }
1548
-
1549
- private consumePendingChildPlaySuspension(): PlayExecutionSuspension | null {
1550
- if (this.pendingChildPlayBoundaries.length === 0) return null;
1551
- const boundaries = [
1552
- ...new Map(
1553
- this.pendingChildPlayBoundaries.map((boundary) => [
1554
- boundary.boundaryId,
1555
- boundary,
1556
- ]),
1557
- ).values(),
1558
- ];
1559
- this.pendingChildPlayBoundaries = [];
1560
- if (boundaries.length === 1) {
1561
- const boundary = boundaries[0]!;
1562
- return {
1563
- kind: 'child_play',
1564
- boundaryId: boundary.boundaryId,
1565
- childRunId: boundary.childRunId,
1566
- childPlayName: boundary.childPlayName,
1567
- };
1568
- }
1569
- return {
1570
- kind: 'child_play_batch',
1571
- boundaries,
1572
- };
1573
- }
1574
-
1575
- private childPlayBoundaryOutput<TOutput>(input: {
1576
- boundaryId: string;
1577
- childRunId: string;
1578
- childPlayName: string;
1579
- }): { found: true; output: TOutput } | { found: false } {
1580
- const existing = this.checkpoint.resolvedBoundaries?.[input.boundaryId];
1581
- if (!existing || existing.kind !== 'child_play') return { found: false };
1582
- if (existing.childRunId !== input.childRunId) return { found: false };
1583
- if (existing.status === 'completed') {
1584
- return { found: true, output: existing.output as TOutput };
1585
- }
1586
- const message =
1587
- typeof existing.error === 'string' && existing.error.trim()
1588
- ? existing.error.trim()
1589
- : `Child play ${input.childPlayName} (${input.childRunId}) ${existing.status}.`;
1590
- throw new Error(message);
1591
- }
1592
-
1593
1440
  private recordPlayCallStep(input: {
1594
1441
  playId: string;
1595
- execution?: 'inline' | 'child-workflow';
1442
+ execution?: 'inline';
1596
1443
  description?: string | null;
1597
1444
  nestedSteps?: PlayStep[];
1598
1445
  }): void {
@@ -1616,186 +1463,6 @@ export class PlayContextImpl {
1616
1463
  );
1617
1464
  }
1618
1465
 
1619
- private async runtimeApiHeaders(): Promise<Record<string, string>> {
1620
- if (!this.#options.executorToken?.trim()) {
1621
- throw new Error(
1622
- 'ctx.runPlay scheduled child launch requires an executor token.',
1623
- );
1624
- }
1625
- return {
1626
- Authorization: `Bearer ${this.#options.executorToken.trim()}`,
1627
- 'Content-Type': 'application/json',
1628
- ...(this.#options.runtimeSchedulerSchema?.trim()
1629
- ? {
1630
- [RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER]:
1631
- this.#options.runtimeSchedulerSchema.trim(),
1632
- [SYNTHETIC_RUN_HEADER]: '1',
1633
- }
1634
- : {}),
1635
- // Defense-in-depth: pin the child to the parent worker's release lane so
1636
- // a child always finishes on the release that launched its parent, even
1637
- // if the resolver default (schema-derived) would drift. The run route
1638
- // honors this only for internal ctx.runPlay child launches.
1639
- ...(this.#options.absurdReleaseId?.trim()
1640
- ? {
1641
- [ABSURD_RELEASE_OVERRIDE_HEADER]:
1642
- this.#options.absurdReleaseId.trim(),
1643
- }
1644
- : {}),
1645
- ...(await this.vercelProtectionHeaders()),
1646
- };
1647
- }
1648
-
1649
- private runtimeApiBaseUrl(): string {
1650
- const baseUrl = this.#options.baseUrl?.trim();
1651
- if (!baseUrl) {
1652
- throw new Error('ctx.runPlay scheduled child launch requires baseUrl.');
1653
- }
1654
- return baseUrl.replace(/\/$/, '');
1655
- }
1656
-
1657
- private async launchScheduledChildPlay(input: {
1658
- key: string;
1659
- childRunId: string;
1660
- childPlayName: string;
1661
- childGovernance: GovernanceSnapshot;
1662
- childInput: Record<string, unknown>;
1663
- description?: string | null;
1664
- }): Promise<void> {
1665
- const parentGovernance = this.currentGovernance;
1666
- const parentPlayName =
1667
- parentGovernance.currentPlayId ||
1668
- this.currentPlayName ||
1669
- this.#options.playId ||
1670
- 'anonymous-play';
1671
- const governance: PlayCallGovernanceSnapshot = {
1672
- rootRunId: parentGovernance.rootRunId,
1673
- parentRunId: parentGovernance.currentRunId,
1674
- parentPlayName,
1675
- key: input.key,
1676
- ancestryPlayIds: [...parentGovernance.ancestryPlayIds],
1677
- callDepth: input.childGovernance.callDepth,
1678
- playCallCount: input.childGovernance.playCallCount,
1679
- toolCallCount: input.childGovernance.toolCallCount,
1680
- retryCount: input.childGovernance.retryCount,
1681
- descendantCount: input.childGovernance.descendantCount,
1682
- waterfallStepExecutions: input.childGovernance.waterfallStepExecutions,
1683
- };
1684
- const requestBody = JSON.stringify({
1685
- name: input.childPlayName,
1686
- workflowId: input.childRunId,
1687
- input: input.childInput,
1688
- // Inherit the parent's scheduler profile so an `absurd` parent launches
1689
- // its child on the same scheduler. Absent threading defaults to absurd.
1690
- profile: this.#options.childRunProfile?.trim() || 'absurd',
1691
- waitForCompletionMs: 0,
1692
- internalRunPlay: {
1693
- ...governance,
1694
- description: input.description ?? null,
1695
- },
1696
- });
1697
- for (let attempt = 0; ; attempt += 1) {
1698
- const response = await fetch(
1699
- `${this.runtimeApiBaseUrl()}/api/v2/plays/run`,
1700
- {
1701
- method: 'POST',
1702
- headers: await this.runtimeApiHeaders(),
1703
- body: requestBody,
1704
- },
1705
- );
1706
- if (response.ok) {
1707
- if (attempt > 0) {
1708
- this.log(
1709
- `ctx.runPlay(${input.key}): child launch recovered after ${attempt} retry(s)`,
1710
- );
1711
- }
1712
- return;
1713
- }
1714
- const body = (await response.json().catch(() => null)) as {
1715
- error?: unknown;
1716
- } | null;
1717
- const message =
1718
- typeof body?.error === 'string' && body.error.trim()
1719
- ? body.error.trim()
1720
- : response.statusText;
1721
- const retryDelayMs = childPlayLaunchRetryDelayMs({
1722
- status: response.status,
1723
- message,
1724
- attempt,
1725
- });
1726
- if (retryDelayMs === null) {
1727
- throw new Error(
1728
- `ctx.runPlay(${input.key}) failed to launch child play "${input.childPlayName}": ${message} (status ${response.status}).`,
1729
- );
1730
- }
1731
- const retryReason =
1732
- response.status === 429 && isChildPlayLaunchAdmissionMessage(message)
1733
- ? 'start admission backpressure'
1734
- : 'transient write conflict';
1735
- this.log(
1736
- `ctx.runPlay(${input.key}): retrying child launch after ${retryReason} (attempt ${attempt + 1})`,
1737
- );
1738
- await sleepTransientRuntimeMs(retryDelayMs);
1739
- }
1740
- }
1741
-
1742
- private async readScheduledChildTerminal<TOutput>(input: {
1743
- childRunId: string;
1744
- childPlayName: string;
1745
- }): Promise<
1746
- | { status: 'completed'; output: TOutput }
1747
- | { status: 'failed' | 'cancelled'; error: string }
1748
- | null
1749
- > {
1750
- const response = await fetch(
1751
- `${this.runtimeApiBaseUrl()}${PLAY_RUNTIME_API_COMPAT_PATH}`,
1752
- {
1753
- method: 'POST',
1754
- headers: await this.runtimeApiHeaders(),
1755
- body: JSON.stringify({
1756
- action: 'read_child_run_terminal_snapshot',
1757
- parentRunId: this.currentGovernance.currentRunId,
1758
- childRunId: input.childRunId,
1759
- childPlayName: input.childPlayName,
1760
- }),
1761
- },
1762
- );
1763
- const body = (await response.json().catch(() => null)) as {
1764
- state?: {
1765
- data?: {
1766
- status?: unknown;
1767
- result?: unknown;
1768
- error?: unknown;
1769
- };
1770
- };
1771
- error?: unknown;
1772
- } | null;
1773
- if (!response.ok) {
1774
- const message =
1775
- typeof body?.error === 'string' && body.error.trim()
1776
- ? body.error.trim()
1777
- : response.statusText;
1778
- throw new Error(
1779
- `ctx.runPlay failed to read child terminal snapshot for "${input.childPlayName}": ${message} (status ${response.status}).`,
1780
- );
1781
- }
1782
- const data = body?.state?.data;
1783
- const status = String(data?.status ?? '').toLowerCase();
1784
- if (status === 'completed') {
1785
- return { status, output: data?.result as TOutput };
1786
- }
1787
- if (status === 'failed' || status === 'cancelled') {
1788
- return {
1789
- status,
1790
- error:
1791
- typeof data?.error === 'string' && data.error.trim()
1792
- ? data.error.trim()
1793
- : `Child play ${input.childPlayName} (${input.childRunId}) ${status}.`,
1794
- };
1795
- }
1796
- return null;
1797
- }
1798
-
1799
1466
  private emitScopedRowUpdate(
1800
1467
  key: string | null,
1801
1468
  tableNamespace: string | null,
@@ -3703,6 +3370,7 @@ export class PlayContextImpl {
3703
3370
  path: string,
3704
3371
  _options?: CsvOptions,
3705
3372
  ): Promise<PlayDataset<Record<string, unknown>>> {
3373
+ this.assertInlineChildContract('dataset_child');
3706
3374
  void _options;
3707
3375
  // In cloud mode, CSV data is passed in — path is just a label
3708
3376
  // The activity loads the actual data before creating the ctx
@@ -3731,6 +3399,7 @@ export class PlayContextImpl {
3731
3399
  input?: MapFieldDefinition<T, TColumns> | RuntimeStepProgram,
3732
3400
  options?: DatasetOptions<T>,
3733
3401
  ): RuntimeDatasetBuilder<T> | Promise<PlayDataset<Record<string, unknown>>> {
3402
+ this.assertInlineChildContract('dataset_child');
3734
3403
  if (rowContext.getStore()) {
3735
3404
  throw new Error(
3736
3405
  'Nested ctx.dataset() is not supported. Flatten your fields into one dataset, or keep custom per-row logic inside a single column.',
@@ -6319,13 +5988,6 @@ export class PlayContextImpl {
6319
5988
  this.toolCallQueue.length === 0 &&
6320
5989
  inFlightToolExecutions.size === 0
6321
5990
  ) {
6322
- if (!this.activeDatasetStep) {
6323
- const childPlaySuspension =
6324
- this.consumePendingChildPlaySuspension();
6325
- if (childPlaySuspension) {
6326
- throw new PlayExecutionSuspendedError(childPlaySuspension);
6327
- }
6328
- }
6329
5991
  break;
6330
5992
  }
6331
5993
 
@@ -6727,6 +6389,7 @@ export class PlayContextImpl {
6727
6389
  input: Record<string, unknown>,
6728
6390
  handler: IntegrationEventWaitHandler,
6729
6391
  ): Promise<unknown> {
6392
+ this.assertInlineChildContract('suspending_child');
6730
6393
  if (!this.#options.durableBoundaries) {
6731
6394
  throw new Error(`${toolId} requires durable play boundaries.`);
6732
6395
  }
@@ -6845,328 +6508,136 @@ export class PlayContextImpl {
6845
6508
  if (!resolvedName.trim()) {
6846
6509
  throw new Error('ctx.runPlay(...) requires a resolvable play name.');
6847
6510
  }
6848
- let scheduledChildPlayCall: { release(): void } | null = null;
6511
+ if (!this.#options.resolvePlay) {
6512
+ throw new Error(
6513
+ 'ctx.runPlay(...) is unavailable because no play resolver was configured.',
6514
+ );
6515
+ }
6516
+ const resolvedPlay = await this.#options.resolvePlay(resolvedName);
6517
+ if (!resolvedPlay) {
6518
+ throw new Error(
6519
+ `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
6520
+ );
6521
+ }
6522
+ const childExecutionDecision = resolveChildExecutionStrategy({
6523
+ pipeline: resolvedPlay.staticPipeline,
6524
+ timeoutMs: options?.timeoutMs,
6525
+ hasExplicitTimeout:
6526
+ options != null && Object.hasOwn(options, 'timeoutMs'),
6527
+ execution: options?.execution,
6528
+ childPlayName: resolvedName,
6529
+ });
6530
+ this.log(
6531
+ `ctx.runPlay(${normalizedKey}): ${childExecutionDecision.strategy} (${childExecutionDecision.reason})`,
6532
+ );
6849
6533
 
6850
- try {
6851
- if (!this.#options.resolvePlay) {
6852
- throw new Error(
6853
- 'ctx.runPlay(...) is unavailable because no play resolver was configured.',
6854
- );
6855
- }
6856
- const resolvePlay = this.#options.resolvePlay;
6857
- const resolvedPlay = await resolvePlay(resolvedName);
6858
- if (!resolvedPlay) {
6859
- throw new Error(
6860
- `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
6861
- );
6862
- }
6863
- const childExecutionDecision = resolveChildExecutionStrategy({
6864
- pipeline: resolvedPlay.staticPipeline,
6865
- execution: options?.execution,
6534
+ const compositionNamespace = this.inlineChildCompositionNamespace(
6535
+ resolvedName,
6536
+ normalizedKey,
6537
+ );
6538
+ const inlineChildGovernor =
6539
+ await this.currentExecutionGovernor.forkInlineChild({
6866
6540
  childPlayName: resolvedName,
6541
+ childRunId: compositionNamespace,
6867
6542
  });
6868
- const launchThroughScheduler =
6869
- this.shouldLaunchChildPlaysThroughScheduler() &&
6870
- childExecutionDecision.strategy === 'scheduled';
6871
- scheduledChildPlayCall = launchThroughScheduler
6872
- ? this.beginScheduledChildPlayCall()
6873
- : null;
6874
- this.log(
6875
- `ctx.runPlay(${normalizedKey}): ${childExecutionDecision.strategy} (${childExecutionDecision.reason})`,
6876
- );
6877
- const runPlayRowScope = rowContext.getStore();
6878
- // Runtime enforcement of the row-scoped-scheduled-child rule for children
6879
- // whose name is only known at runtime (static preflight cannot see them).
6880
- // A scheduling-required child fanned out per row is the 100,000-run-doc
6881
- // failure mode the inline-child cutover removes. Fail on the FIRST row,
6882
- // loud, with the SAME message the static preflight emits. See ADR 0013.
6883
- //
6884
- // Mirrors the static check's proof standard: reject when the child's
6885
- // resolved contract PROVES it needs scheduling (dataset/csv, event-wait,
6886
- // explicit timeout), or when this call would actually launch a durable
6887
- // scheduled child run per row. A missing static contract alone is not
6888
- // proof — in-process fallback runtimes legitimately run contract-less
6889
- // scalar children inline.
6890
- const provablySchedulingRequired =
6891
- childExecutionDecision.strategy === 'scheduled' &&
6892
- childExecutionDecision.reason !== 'missing_static_contract';
6893
- if (
6894
- runPlayRowScope &&
6895
- (provablySchedulingRequired || launchThroughScheduler)
6896
- ) {
6897
- throw new Error(
6898
- inlineChildScheduledInRowMessage(
6899
- resolvedName,
6900
- runPlayRowScope.tableNamespace ??
6901
- runPlayRowScope.fieldName ??
6902
- 'dataset',
6903
- ),
6904
- );
6543
+ const rowStore = rowContext.getStore();
6544
+ const producer = {
6545
+ kind: 'play' as const,
6546
+ id: normalizedKey,
6547
+ playId: resolvedName,
6548
+ displayName: displayNameFromProducerId(resolvedName),
6549
+ runId: compositionNamespace,
6550
+ };
6551
+ try {
6552
+ if (rowStore) {
6553
+ this.emitScopedFieldMetaUpdate({
6554
+ rowId: rowStore.rowId,
6555
+ key: rowStore.rowKey ?? null,
6556
+ tableNamespace: rowStore.tableNamespace ?? null,
6557
+ fieldName: rowStore.fieldName,
6558
+ status: 'running',
6559
+ rowStatus: 'running',
6560
+ stage: resolvedName,
6561
+ provider: 'deepline_native',
6562
+ error: null,
6563
+ producer,
6564
+ dataPatch: {},
6565
+ });
6905
6566
  }
6906
- const runPlayInvocationScope = buildDurableRunPlayInvocationScope({
6907
- childPlayName: resolvedName,
6908
- input,
6909
- rowScope: runPlayRowScope
6910
- ? {
6911
- fieldName: runPlayRowScope.fieldName,
6912
- rowKey: runPlayRowScope.rowKey ?? null,
6913
- tableNamespace: runPlayRowScope.tableNamespace ?? null,
6914
- }
6915
- : null,
6916
- });
6917
-
6918
- const executePlayCall = async (): Promise<TOutput> => {
6919
- // Scheduled children reserve the parent's durable play/descendant
6920
- // budgets. Inline scalar children are function composition: their
6921
- // Governor view changes only local lineage for recursion/depth checks
6922
- // and delegates tool admission, pacing, and tool budgets to the parent.
6923
- //
6924
- // The SCHEDULED substrate still allocates a durable child run id (through
6925
- // the shared canonical builder so in-process and the workers_edge
6926
- // coordinator produce the same FORMAT `play/<slug>/run/child-<digest>`).
6927
- // INLINE scalar children are pure function composition (ADR 0013): they
6928
- // carry NO durable run identity — no child run id, no playRuns doc, no
6929
- // settlement. Their only identity is a replay-stable, call-site scoped
6930
- // composition namespace (`child:<playName>#<ordinal>`) used to scope tool
6931
- // receipts under the PARENT run. See child-composition-namespace below.
6932
- const parentGovernor = this.currentExecutionGovernor;
6933
- const inlineChildGovernor = launchThroughScheduler
6934
- ? null
6935
- : await parentGovernor.forkInlineChild({
6936
- childPlayName: resolvedName,
6937
- childRunId: this.inlineChildCompositionNamespace(
6938
- resolvedName,
6939
- normalizedKey,
6940
- ),
6941
- });
6942
- const childGovernance = launchThroughScheduler
6943
- ? await parentGovernor.forkChild({
6944
- childPlayName: resolvedName,
6945
- childRunId: buildChildRunId({
6946
- childPlayName: resolvedName,
6947
- parentRunId: this.currentRunId,
6948
- parentPlayName: this.#options.playName,
6949
- key: normalizedKey,
6950
- runPlaySemanticKey: runPlayInvocationScope,
6951
- input,
6952
- graphHash: null,
6953
- }),
6954
- })
6955
- : inlineChildGovernor!.snapshot();
6956
- const childPlaySlot = launchThroughScheduler
6957
- ? await parentGovernor.acquireChildSubmitSlot()
6958
- : null;
6959
- const rowStore = rowContext.getStore();
6960
- const producer = {
6961
- kind: 'play' as const,
6962
- id: normalizedKey,
6567
+ // Inline composition has no child run, launch request, terminal poll, or
6568
+ // scheduler slot. It shares the caller's execution and tool governors.
6569
+ const childExecutionScope = deriveChildRunExecutionScope(
6570
+ this.currentExecutionScope,
6571
+ {
6572
+ placement: 'inline',
6573
+ runId: compositionNamespace,
6963
6574
  playId: resolvedName,
6964
- displayName: displayNameFromProducerId(resolvedName),
6965
- // For scheduled children this is the durable child run id; for inline
6966
- // children it is the composition namespace (`child:<playName>#<ord>`).
6967
- runId: childGovernance.currentRunId,
6968
- };
6969
- try {
6970
- if (rowStore) {
6971
- this.emitScopedFieldMetaUpdate({
6972
- rowId: rowStore.rowId,
6973
- key: rowStore.rowKey ?? null,
6974
- tableNamespace: rowStore.tableNamespace ?? null,
6975
- fieldName: rowStore.fieldName,
6976
- status: 'running',
6977
- rowStatus: 'running',
6978
- stage: resolvedName,
6979
- provider: 'deepline_native',
6980
- error: null,
6981
- producer,
6982
- dataPatch: {},
6983
- });
6984
- }
6985
- if (launchThroughScheduler) {
6986
- const boundaryId = this.childPlayBoundaryId(
6987
- childGovernance.currentRunId,
6988
- );
6989
- const checkpointResult = this.childPlayBoundaryOutput<TOutput>({
6990
- boundaryId,
6991
- childRunId: childGovernance.currentRunId,
6992
- childPlayName: resolvedName,
6993
- });
6994
- if (checkpointResult.found) {
6995
- if (rowStore) {
6996
- this.emitScopedFieldMetaUpdate({
6997
- rowId: rowStore.rowId,
6998
- key: rowStore.rowKey ?? null,
6999
- tableNamespace: rowStore.tableNamespace ?? null,
7000
- fieldName: rowStore.fieldName,
7001
- status: 'completed',
7002
- rowStatus: 'running',
7003
- stage: resolvedName,
7004
- provider: 'deepline_native',
7005
- error: null,
7006
- producer,
7007
- dataPatch: {},
7008
- });
7009
- }
7010
- this.recordPlayCallStep({
7011
- playId: resolvedName,
7012
- execution: options?.execution,
7013
- description: options?.description,
7014
- });
7015
- return checkpointResult.output;
7016
- }
7017
-
7018
- await this.launchScheduledChildPlay({
7019
- key: normalizedKey,
7020
- childRunId: childGovernance.currentRunId,
7021
- childPlayName: resolvedName,
7022
- childGovernance,
7023
- childInput: input,
7024
- description: options?.description ?? null,
7025
- });
7026
- childPlaySlot?.release();
7027
- const terminal = await this.readScheduledChildTerminal<TOutput>({
7028
- childRunId: childGovernance.currentRunId,
7029
- childPlayName: resolvedName,
7030
- });
7031
- if (terminal?.status === 'completed') {
7032
- this.checkpoint.resolvedBoundaries = {
7033
- ...(this.checkpoint.resolvedBoundaries ?? {}),
7034
- [boundaryId]: {
7035
- kind: 'child_play',
7036
- childRunId: childGovernance.currentRunId,
7037
- childPlayName: resolvedName,
7038
- status: 'completed',
7039
- output: terminal.output,
7040
- completedAt: Date.now(),
7041
- },
7042
- };
7043
- if (rowStore) {
7044
- this.emitScopedFieldMetaUpdate({
7045
- rowId: rowStore.rowId,
7046
- key: rowStore.rowKey ?? null,
7047
- tableNamespace: rowStore.tableNamespace ?? null,
7048
- fieldName: rowStore.fieldName,
7049
- status: 'completed',
7050
- rowStatus: 'running',
7051
- stage: resolvedName,
7052
- provider: 'deepline_native',
7053
- error: null,
7054
- producer,
7055
- dataPatch: {},
7056
- });
7057
- }
7058
- this.recordPlayCallStep({
7059
- playId: resolvedName,
7060
- execution: options?.execution,
7061
- description: options?.description,
7062
- });
7063
- return terminal.output;
7064
- }
7065
- if (terminal) {
7066
- throw new Error(terminal.error);
7067
- }
7068
-
7069
- const suspension = {
7070
- kind: 'child_play',
7071
- boundaryId,
7072
- childRunId: childGovernance.currentRunId,
7073
- childPlayName: resolvedName,
7074
- } as const;
7075
- this.recordPendingChildPlayBoundary(suspension);
7076
- scheduledChildPlayCall?.release();
7077
- await this.waitForScheduledChildPlayQuiescence();
7078
- throw new PlayExecutionSuspendedError(
7079
- this.consumePendingChildPlaySuspension() ?? suspension,
7080
- );
7081
- }
7082
- // Inline child = pure function composition (ADR 0013). No durable
7083
- // child run: no playRuns doc, no start/settle round-trip, no
7084
- // OptimisticConcurrencyControlFailure fan-out. The child's tool
7085
- // receipts still land durably under the PARENT run (the inline
7086
- // execution scope keeps the parent's receipt.ownerRunId) scoped by the
7087
- // replay-stable composition namespace (which also enforces the
7088
- // per-row inline-invocation sanity cap at creation).
7089
- const compositionNamespace = childGovernance.currentRunId;
7090
- const childExecutionScope = deriveChildRunExecutionScope(
7091
- this.currentExecutionScope,
7092
- {
7093
- placement: 'inline',
7094
- runId: compositionNamespace,
7095
- playId: resolvedName,
7096
- receiptNamespace: compositionNamespace,
7097
- },
7098
- );
7099
- this.inlineChildAggregates.total += 1;
7100
- try {
7101
- // A scalar child is a normal function call on the parent's context.
7102
- // Async-local identity changes receipt and recursion scope without
7103
- // allocating another PlayContext, tool queue, drain loop, resource
7104
- // governor, or receipt client. Calls from every concurrent child
7105
- // therefore reach the same proven parent batching path.
7106
- const result = await inlineCompositionContext.run(
7107
- {
7108
- context: this,
7109
- executionScope: childExecutionScope,
7110
- governor: inlineChildGovernor!,
7111
- playName: resolvedName,
7112
- staticPipeline: resolvedPlay.staticPipeline ?? null,
7113
- },
7114
- () => this.executeResolvedPlay(resolvedPlay, this, input),
7115
- );
7116
- this.inlineChildAggregates.ok += 1;
7117
- if (rowStore) {
7118
- this.emitScopedFieldMetaUpdate({
7119
- rowId: rowStore.rowId,
7120
- key: rowStore.rowKey ?? null,
7121
- tableNamespace: rowStore.tableNamespace ?? null,
7122
- fieldName: rowStore.fieldName,
7123
- status: 'completed',
7124
- rowStatus: 'running',
7125
- stage: resolvedName,
7126
- provider: 'deepline_native',
7127
- error: null,
7128
- producer,
7129
- dataPatch: {},
7130
- });
7131
- }
7132
- this.recordPlayCallStep({
7133
- playId: resolvedName,
7134
- execution: options?.execution,
7135
- description: options?.description,
7136
- });
7137
- return result as TOutput;
7138
- } catch (childError) {
7139
- this.recordInlineChildFailure(resolvedName, childError);
7140
- throw childError;
7141
- }
7142
- } catch (error) {
7143
- if (isPlayExecutionSuspendedError(error)) {
7144
- throw error;
7145
- }
7146
- if (rowStore) {
7147
- this.emitScopedFieldMetaUpdate({
7148
- rowId: rowStore.rowId,
7149
- key: rowStore.rowKey ?? null,
7150
- tableNamespace: rowStore.tableNamespace ?? null,
7151
- fieldName: rowStore.fieldName,
7152
- status: 'failed',
7153
- rowStatus: 'running',
7154
- stage: resolvedName,
7155
- provider: 'deepline_native',
7156
- error: this.formatRuntimeError(error),
7157
- producer,
7158
- dataPatch: {},
7159
- });
7160
- }
7161
- throw error;
7162
- } finally {
7163
- childPlaySlot?.release();
6575
+ receiptNamespace: compositionNamespace,
6576
+ },
6577
+ );
6578
+ this.inlineChildAggregates.total += 1;
6579
+ try {
6580
+ // A scalar child is a normal function call on the parent's context.
6581
+ // Async-local identity changes receipt and recursion scope without
6582
+ // allocating another PlayContext, tool queue, drain loop, resource
6583
+ // governor, or receipt client. Calls from every concurrent child
6584
+ // therefore reach the same proven parent batching path.
6585
+ const result = await inlineCompositionContext.run(
6586
+ {
6587
+ context: this,
6588
+ executionScope: childExecutionScope,
6589
+ governor: inlineChildGovernor,
6590
+ playName: resolvedName,
6591
+ staticPipeline: resolvedPlay.staticPipeline ?? null,
6592
+ },
6593
+ () => this.executeResolvedPlay(resolvedPlay, this, input),
6594
+ );
6595
+ this.inlineChildAggregates.ok += 1;
6596
+ if (rowStore) {
6597
+ this.emitScopedFieldMetaUpdate({
6598
+ rowId: rowStore.rowId,
6599
+ key: rowStore.rowKey ?? null,
6600
+ tableNamespace: rowStore.tableNamespace ?? null,
6601
+ fieldName: rowStore.fieldName,
6602
+ status: 'completed',
6603
+ rowStatus: 'running',
6604
+ stage: resolvedName,
6605
+ provider: 'deepline_native',
6606
+ error: null,
6607
+ producer,
6608
+ dataPatch: {},
6609
+ });
7164
6610
  }
7165
- };
7166
-
7167
- return await executePlayCall();
7168
- } finally {
7169
- scheduledChildPlayCall?.release();
6611
+ this.recordPlayCallStep({
6612
+ playId: resolvedName,
6613
+ execution: options?.execution === 'inline' ? 'inline' : undefined,
6614
+ description: options?.description,
6615
+ });
6616
+ return result as TOutput;
6617
+ } catch (childError) {
6618
+ this.recordInlineChildFailure(resolvedName, childError);
6619
+ throw childError;
6620
+ }
6621
+ } catch (error) {
6622
+ if (isPlayExecutionSuspendedError(error)) {
6623
+ throw error;
6624
+ }
6625
+ if (rowStore) {
6626
+ this.emitScopedFieldMetaUpdate({
6627
+ rowId: rowStore.rowId,
6628
+ key: rowStore.rowKey ?? null,
6629
+ tableNamespace: rowStore.tableNamespace ?? null,
6630
+ fieldName: rowStore.fieldName,
6631
+ status: 'failed',
6632
+ rowStatus: 'running',
6633
+ stage: resolvedName,
6634
+ provider: 'deepline_native',
6635
+ error: this.formatRuntimeError(error),
6636
+ producer,
6637
+ dataPatch: {},
6638
+ });
6639
+ }
6640
+ throw error;
7170
6641
  }
7171
6642
  }
7172
6643
 
@@ -7200,25 +6671,17 @@ export class PlayContextImpl {
7200
6671
  const namespace = `child:${childPlayName}#${normalizedKey}${
7201
6672
  rowScopeKey ? `@${rowScopeKey}` : ''
7202
6673
  }`;
7203
- if (!this.inlineChildInvocationOrdinals.has(namespace)) {
7204
- // Per-row fan-width guard. `maxPlayCallDepth` (governor) bounds nesting;
7205
- // this bounds distinct inline invocations from ONE row scope so a
7206
- // resolver looping runPlay unbounded fails loudly on that row instead of
7207
- // leaking namespaces.
7208
- const rowInvocationCount =
7209
- (this.inlineChildInvocationsPerRow.get(rowScopeKey) ?? 0) + 1;
7210
- if (rowInvocationCount > MAX_INLINE_CHILD_INVOCATIONS_PER_ROW) {
6674
+ if (rowScope) {
6675
+ const namespaces =
6676
+ rowScope.inlineChildInvocationNamespaces ??= new Set<string>();
6677
+ namespaces.add(namespace);
6678
+ if (namespaces.size > MAX_INLINE_CHILD_INVOCATIONS_PER_ROW) {
7211
6679
  throw new Error(
7212
6680
  `ctx.runPlay("${childPlayName}") exceeded the inline child invocation cap ` +
7213
6681
  `(${MAX_INLINE_CHILD_INVOCATIONS_PER_ROW} per row). A resolver is calling ` +
7214
6682
  'runPlay in an unbounded loop; give the batch ONE dataset child at play level instead.',
7215
6683
  );
7216
6684
  }
7217
- this.inlineChildInvocationsPerRow.set(rowScopeKey, rowInvocationCount);
7218
- this.inlineChildInvocationOrdinals.set(
7219
- namespace,
7220
- this.inlineChildInvocationOrdinals.size,
7221
- );
7222
6685
  }
7223
6686
  return namespace;
7224
6687
  }
@@ -7306,6 +6769,7 @@ export class PlayContextImpl {
7306
6769
  }
7307
6770
 
7308
6771
  async sleep(ms: number): Promise<void> {
6772
+ this.assertInlineChildContract('suspending_child');
7309
6773
  if (this.#options.durableBoundaries) {
7310
6774
  const delayMs = Math.max(0, Math.round(ms));
7311
6775
  const boundaryId = this.durableBoundaryId(