deepline 0.1.207 → 0.1.209

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 (33) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +2 -12
  2. package/dist/bundling-sources/sdk/src/client.ts +10 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +10 -1
  4. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +16 -31
  6. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +18 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +312 -207
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +15 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +0 -5
  10. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +429 -102
  12. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +27 -5
  13. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +31 -12
  15. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +103 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +105 -10
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +21 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +32 -86
  19. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +141 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +45 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +180 -447
  22. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +33 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  25. package/dist/bundling-sources/shared_libs/plays/artifact-kind-guard.ts +112 -0
  26. package/dist/cli/index.js +38 -5
  27. package/dist/cli/index.mjs +38 -5
  28. package/dist/index.d.mts +10 -0
  29. package/dist/index.d.ts +10 -0
  30. package/dist/index.js +32 -5
  31. package/dist/index.mjs +32 -5
  32. package/package.json +1 -1
  33. package/dist/bundling-sources/shared_libs/play-runtime/child-run-lifecycle.ts +0 -70
@@ -51,11 +51,6 @@ import {
51
51
  PLAY_RUNTIME_API_COMPAT_PATH,
52
52
  PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
53
53
  } from './runtime-api-paths';
54
- import {
55
- settleInlineChildRunViaAppRuntime,
56
- startRunViaAppRuntime,
57
- } from './app-runtime-api';
58
- import { executeChildRunLifecycle } from './child-run-lifecycle';
59
54
  import {
60
55
  createRootRunExecutionScope,
61
56
  deriveChildRunExecutionScope,
@@ -79,6 +74,7 @@ import {
79
74
  type RuntimeResourceGovernor,
80
75
  } from './resource-governor';
81
76
  import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
77
+ import { ProviderExhaustedError } from './run-failure';
82
78
  import { InMemoryRateStateBackend } from './governor/in-memory-rate-state-backend';
83
79
  import { pacingPolicyForTool } from './pacing';
84
80
  import {
@@ -136,7 +132,7 @@ import {
136
132
  isQueryResultDatasetReadRequest,
137
133
  isQueryResultDatasetTool,
138
134
  } from './query-result-dataset';
139
- import { isAbortLikeError, isRowIsolationExemptError } from './row-isolation';
135
+ import { isRowIsolationExemptError } from './row-isolation';
140
136
  import {
141
137
  createRuntimePersistenceLatch,
142
138
  RuntimePersistenceCircuitOpenError,
@@ -241,7 +237,10 @@ import {
241
237
  type StepProgramDatasetOptions,
242
238
  } from './step-program-dataset-builder';
243
239
  import { readRuntimeSheetDatasetRows } from './runtime-api';
244
- import { resolveChildExecutionStrategy } from './child-execution-strategy';
240
+ import {
241
+ resolveChildExecutionStrategy,
242
+ inlineChildScheduledInRowMessage,
243
+ } from './child-execution-strategy';
245
244
 
246
245
  type ResolvedPlayExecutor = (
247
246
  ctx: PlayContextImpl,
@@ -292,6 +291,14 @@ const CHILD_PLAY_LAUNCH_ADMISSION_RETRY_DELAYS_MS = [
292
291
  ] as const;
293
292
  const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
294
293
  const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
294
+ // Per-row sanity cap on distinct inline child invocations. `maxPlayCallDepth`
295
+ // (governor) already bounds nesting; this bounds fan-WIDTH from a single row so
296
+ // a resolver that loops `ctx.runPlay` unbounded fails loudly instead of leaking
297
+ // composition namespaces. Well above any legitimate authored per-row fan-out.
298
+ const MAX_INLINE_CHILD_INVOCATIONS_PER_ROW = 512;
299
+ // Bound the retained failure detail carried in parent aggregates so a fully
300
+ // failing large map cannot grow the progress event without limit.
301
+ const MAX_INLINE_CHILD_FAILURE_DETAIL = 25;
295
302
  // A single batched runtime-step-receipt request (get/claim/complete/fail/seed)
296
303
  // carries at most this many keys. At map scale a run can hold 5k-10k receipt
297
304
  // keys; sending them all in one request made the server claim/complete them
@@ -1088,6 +1095,30 @@ export class PlayContextImpl {
1088
1095
  createSecretRedactionContext();
1089
1096
  private mapInvocationIndex = 0;
1090
1097
  private readonly stepCallIndexByKey = new Map<string, number>();
1098
+ /**
1099
+ * Registry of distinct inline child invocation namespaces seen by this
1100
+ * context. Inline child composition is a function call, not a run; each
1101
+ * invocation's namespace (`child:<playName>#<callKey>[@<rowScope>]`) scopes
1102
+ * its receipts under the parent run. See ADR 0013.
1103
+ */
1104
+ private readonly inlineChildInvocationOrdinals = new Map<string, number>();
1105
+ /**
1106
+ * Distinct inline child invocations per row scope (top-level counts as one
1107
+ * scope). Backs the per-row fan-width sanity cap so a resolver looping
1108
+ * `ctx.runPlay` unbounded fails loudly instead of leaking namespaces.
1109
+ */
1110
+ private readonly inlineChildInvocationsPerRow = new Map<string, number>();
1111
+ /**
1112
+ * Parent-level inline-child aggregates. Maintained only by the single-writer
1113
+ * progress path (never per child) so concurrent fan-out cannot contend. See
1114
+ * `flushInlineChildAggregates` and ADR 0013.
1115
+ */
1116
+ private inlineChildAggregates: {
1117
+ total: number;
1118
+ ok: number;
1119
+ failed: number;
1120
+ failures: Array<{ childPlayName: string; error: string }>;
1121
+ } = { total: 0, ok: 0, failed: 0, failures: [] };
1091
1122
  /**
1092
1123
  * Runtime persistence-failure circuit breaker (postgres_fast parity with the
1093
1124
  * Workers runtime). The first persistence failure — a receipt-completion
@@ -4906,6 +4937,9 @@ export class PlayContextImpl {
4906
4937
  completedRowKeys.size,
4907
4938
  failedRows,
4908
4939
  totalRows,
4940
+ // Inline child aggregates ride the single-writer progress event so
4941
+ // fan-out never contends on a per-child mutation. See ADR 0013.
4942
+ ...this.inlineChildAggregateEventFields(),
4909
4943
  at: Date.now(),
4910
4944
  } as PlayExecutionEvent);
4911
4945
  }
@@ -5448,34 +5482,66 @@ export class PlayContextImpl {
5448
5482
  const produced: Record<string, unknown> = {};
5449
5483
  for (const step of program.steps) {
5450
5484
  const stepPath = [...path, step.name];
5451
- const value = options?.checkpointSteps
5452
- ? await this.step(
5453
- stepPath.join('.'),
5454
- async () =>
5455
- await this.executeStepProgramStep(
5456
- step,
5457
- currentRow,
5458
- index,
5459
- stepPath,
5460
- undefined,
5461
- ),
5462
- {
5463
- semanticKey: stableDigest(
5464
- stableStringify({
5485
+ const runStep = async () =>
5486
+ options?.checkpointSteps
5487
+ ? await this.step(
5488
+ stepPath.join('.'),
5489
+ async () =>
5490
+ await this.executeStepProgramStep(
5491
+ step,
5492
+ currentRow,
5465
5493
  index,
5466
- input: currentRow,
5467
5494
  stepPath,
5468
- }),
5469
- ),
5470
- },
5471
- )
5472
- : await this.executeStepProgramStep(
5473
- step,
5474
- currentRow,
5475
- index,
5476
- stepPath,
5477
- undefined,
5478
- );
5495
+ undefined,
5496
+ ),
5497
+ {
5498
+ semanticKey: stableDigest(
5499
+ stableStringify({
5500
+ index,
5501
+ input: currentRow,
5502
+ stepPath,
5503
+ }),
5504
+ ),
5505
+ },
5506
+ )
5507
+ : await this.executeStepProgramStep(
5508
+ step,
5509
+ currentRow,
5510
+ index,
5511
+ stepPath,
5512
+ undefined,
5513
+ );
5514
+ let value: unknown;
5515
+ try {
5516
+ value = await runStep();
5517
+ } catch (error) {
5518
+ // PROVIDER_EXHAUSTED is a step MISS, not a program abort. The pacer
5519
+ // skipped this provider (no spend, no dispatch); resolve the step as an
5520
+ // empty result so the row falls through to the next step's `runIf` chain
5521
+ // exactly as it would for a provider miss. Any OTHER error keeps its
5522
+ // existing propagation (row-isolation / abort semantics unchanged).
5523
+ if (!(error instanceof ProviderExhaustedError)) throw error;
5524
+ value = null;
5525
+ const rowStore = rowContext.getStore();
5526
+ const fieldName = stepPath.join('.');
5527
+ if (rowStore) {
5528
+ this.emitScopedFieldMetaUpdate({
5529
+ rowId: rowStore.rowId,
5530
+ key: rowStore.rowKey ?? null,
5531
+ tableNamespace: rowStore.tableNamespace ?? null,
5532
+ fieldName,
5533
+ status: 'failed',
5534
+ rowStatus: 'running',
5535
+ stage: 'failed',
5536
+ provider: null,
5537
+ error: this.formatRuntimeError(error),
5538
+ dataPatch: {},
5539
+ });
5540
+ }
5541
+ produced[step.name] = value;
5542
+ currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
5543
+ continue;
5544
+ }
5479
5545
  produced[step.name] = value;
5480
5546
  const rowStore = rowContext.getStore();
5481
5547
  const fieldName = stepPath.join('.');
@@ -6319,6 +6385,34 @@ export class PlayContextImpl {
6319
6385
  `ctx.runPlay(${normalizedKey}): ${childExecutionDecision.strategy} (${childExecutionDecision.reason})`,
6320
6386
  );
6321
6387
  const runPlayRowScope = rowContext.getStore();
6388
+ // Runtime enforcement of the row-scoped-scheduled-child rule for children
6389
+ // whose name is only known at runtime (static preflight cannot see them).
6390
+ // A scheduling-required child fanned out per row is the 100,000-run-doc
6391
+ // failure mode the inline-child cutover removes. Fail on the FIRST row,
6392
+ // loud, with the SAME message the static preflight emits. See ADR 0013.
6393
+ //
6394
+ // Mirrors the static check's proof standard: reject when the child's
6395
+ // resolved contract PROVES it needs scheduling (dataset/csv, event-wait,
6396
+ // explicit timeout), or when this call would actually launch a durable
6397
+ // scheduled child run per row. A missing static contract alone is not
6398
+ // proof — in-process fallback runtimes legitimately run contract-less
6399
+ // scalar children inline.
6400
+ const provablySchedulingRequired =
6401
+ childExecutionDecision.strategy === 'scheduled' &&
6402
+ childExecutionDecision.reason !== 'missing_static_contract';
6403
+ if (
6404
+ runPlayRowScope &&
6405
+ (provablySchedulingRequired || launchThroughScheduler)
6406
+ ) {
6407
+ throw new Error(
6408
+ inlineChildScheduledInRowMessage(
6409
+ resolvedName,
6410
+ runPlayRowScope.tableNamespace ??
6411
+ runPlayRowScope.fieldName ??
6412
+ 'dataset',
6413
+ ),
6414
+ );
6415
+ }
6322
6416
  const runPlaySemanticKey = buildDurableRunPlaySemanticKey({
6323
6417
  childPlayName: resolvedName,
6324
6418
  childRevisionFingerprint,
@@ -6339,33 +6433,35 @@ export class PlayContextImpl {
6339
6433
  // cycle guard, depth cap, per-parent cap, and play/descendant charges, so
6340
6434
  // a recovered receipt (which skips this body) never consumes budget.
6341
6435
  //
6342
- // The child run id is derived through the shared canonical builder so the
6343
- // in-process runtime and the workers_edge coordinator produce the same
6344
- // FORMAT (`play/<slug>/run/child-<digest>`) via the same code. The digest
6345
- // is deterministic across replay because every input (parent lineage,
6346
- // call key, input) is stable. Per-substrate difference: in-process has no
6347
- // coordinator idempotency key and no compiled child graphHash, so it uses
6348
- // the semantic fallback with graphHash omitted; workers_edge feeds the
6349
- // durable child idempotency key. See child-run-id.ts.
6350
- const childRunId = buildChildRunId({
6351
- childPlayName: resolvedName,
6352
- parentRunId: this.currentRunId,
6353
- parentPlayName: this.#options.playName,
6354
- key: normalizedKey,
6355
- runPlaySemanticKey,
6356
- input,
6357
- graphHash: null,
6358
- });
6436
+ // The SCHEDULED substrate still allocates a durable child run id (through
6437
+ // the shared canonical builder so in-process and the workers_edge
6438
+ // coordinator produce the same FORMAT `play/<slug>/run/child-<digest>`).
6439
+ // INLINE scalar children are pure function composition (ADR 0013): they
6440
+ // carry NO durable run identity no child run id, no playRuns doc, no
6441
+ // settlement. Their only identity is a replay-stable, call-site scoped
6442
+ // composition namespace (`child:<playName>#<ordinal>`) used to scope tool
6443
+ // receipts under the PARENT run. See child-composition-namespace below.
6359
6444
  const inlineChildGovernor = launchThroughScheduler
6360
6445
  ? null
6361
6446
  : await this.governor.forkInlineChild({
6362
6447
  childPlayName: resolvedName,
6363
- childRunId,
6448
+ childRunId: this.inlineChildCompositionNamespace(
6449
+ resolvedName,
6450
+ normalizedKey,
6451
+ ),
6364
6452
  });
6365
6453
  const childGovernance = launchThroughScheduler
6366
6454
  ? await this.governor.forkChild({
6367
6455
  childPlayName: resolvedName,
6368
- childRunId,
6456
+ childRunId: buildChildRunId({
6457
+ childPlayName: resolvedName,
6458
+ parentRunId: this.currentRunId,
6459
+ parentPlayName: this.#options.playName,
6460
+ key: normalizedKey,
6461
+ runPlaySemanticKey,
6462
+ input,
6463
+ graphHash: null,
6464
+ }),
6369
6465
  })
6370
6466
  : inlineChildGovernor!.snapshot();
6371
6467
  const childPlaySlot = launchThroughScheduler
@@ -6377,7 +6473,9 @@ export class PlayContextImpl {
6377
6473
  id: normalizedKey,
6378
6474
  playId: resolvedName,
6379
6475
  displayName: displayNameFromProducerId(resolvedName),
6380
- runId: childRunId,
6476
+ // For scheduled children this is the durable child run id; for inline
6477
+ // children it is the composition namespace (`child:<playName>#<ord>`).
6478
+ runId: childGovernance.currentRunId,
6381
6479
  };
6382
6480
  try {
6383
6481
  if (rowStore) {
@@ -6490,112 +6588,93 @@ export class PlayContextImpl {
6490
6588
  this.consumePendingChildPlaySuspension() ?? suspension,
6491
6589
  );
6492
6590
  }
6591
+ // Inline child = pure function composition (ADR 0013). No durable
6592
+ // child run: no playRuns doc, no start/settle round-trip, no
6593
+ // OptimisticConcurrencyControlFailure fan-out. The child's tool
6594
+ // receipts still land durably under the PARENT run (the inline
6595
+ // execution scope keeps the parent's receipt.ownerRunId) scoped by the
6596
+ // replay-stable composition namespace (which also enforces the
6597
+ // per-row inline-invocation sanity cap at creation).
6598
+ const compositionNamespace = childGovernance.currentRunId;
6493
6599
  const childExecutionScope = deriveChildRunExecutionScope(
6494
6600
  this.executionScope,
6495
6601
  {
6496
6602
  placement: 'inline',
6497
- runId: childGovernance.currentRunId,
6603
+ runId: compositionNamespace,
6498
6604
  playId: resolvedName,
6499
- receiptNamespace: `${resolvedName}:${childGovernance.currentRunId}`,
6605
+ receiptNamespace: compositionNamespace,
6500
6606
  },
6501
6607
  );
6502
- return await executeChildRunLifecycle({
6503
- scope: childExecutionScope,
6504
- metadata: {
6505
- placement: 'inline',
6506
- childPlayName: resolvedName,
6507
- staticPipeline: resolvedPlay.staticPipeline ?? null,
6508
- },
6509
- adapter: {
6510
- open: async () => {
6511
- await this.registerInlineChildRun({
6512
- scope: childExecutionScope,
6513
- staticPipeline: resolvedPlay.staticPipeline ?? null,
6514
- });
6515
- },
6516
- // start_inline_child_run records run.started with the durable row.
6517
- started: () => undefined,
6518
- complete: async (_scope, _metadata, result) => {
6519
- await this.settleInlineChildRun({
6520
- scope: childExecutionScope,
6521
- status: 'completed',
6522
- result,
6523
- });
6524
- },
6525
- fail: async (_scope, _metadata, error) => {
6526
- await this.settleInlineChildRun({
6527
- scope: childExecutionScope,
6528
- status: isAbortLikeError(error) ? 'cancelled' : 'failed',
6529
- error: this.formatRuntimeError(error),
6530
- });
6531
- },
6532
- },
6533
- execute: async () => {
6534
- const inlineScalarChild =
6535
- childExecutionDecision.reason === 'scalar_child';
6536
- const childContext = createPlayContext({
6537
- ...this.#options,
6538
- executionScope: childExecutionScope,
6539
- playId: resolvedName,
6540
- playName: resolvedName,
6541
- runId: childGovernance.currentRunId,
6542
- executorToken: inlineScalarChild
6543
- ? this.#options.executorToken
6544
- : await this.mintChildExecutorToken({
6545
- parentRunId: this.currentRunId,
6546
- parentPlayName: this.#options.playName,
6547
- childRunId: childGovernance.currentRunId,
6548
- childPlayName: resolvedName,
6549
- }),
6550
- staticPipeline: resolvedPlay.staticPipeline ?? null,
6551
- checkpoint: this.checkpoint,
6552
- governance: undefined,
6553
- executionGovernor: inlineChildGovernor!,
6554
- ...(inlineScalarChild
6555
- ? {}
6556
- : {
6557
- getRuntimeStepReceipt: undefined,
6558
- getRuntimeStepReceipts: undefined,
6559
- claimRuntimeStepReceipt: undefined,
6560
- claimRuntimeStepReceipts: undefined,
6561
- completeRuntimeStepReceipt: undefined,
6562
- completeRuntimeStepReceipts: undefined,
6563
- failRuntimeStepReceipt: undefined,
6564
- failRuntimeStepReceipts: undefined,
6565
- heartbeatRuntimeStepReceipts: undefined,
6566
- skipRuntimeStepReceipt: undefined,
6567
- }),
6568
- });
6569
- const childExecution = this.executeResolvedPlay(
6570
- resolvedPlay,
6571
- childContext,
6572
- input,
6573
- );
6574
- await childContext.drainQueuedWork([childExecution]);
6575
- const result = await childExecution;
6576
- if (rowStore) {
6577
- this.emitScopedFieldMetaUpdate({
6578
- rowId: rowStore.rowId,
6579
- key: rowStore.rowKey ?? null,
6580
- tableNamespace: rowStore.tableNamespace ?? null,
6581
- fieldName: rowStore.fieldName,
6582
- status: 'completed',
6583
- rowStatus: 'running',
6584
- stage: resolvedName,
6585
- provider: 'deepline_native',
6586
- error: null,
6587
- producer,
6588
- dataPatch: {},
6589
- });
6590
- }
6591
- this.recordPlayCallStep({
6592
- playId: resolvedName,
6593
- description: options?.description,
6594
- nestedSteps: childContext.getSteps(),
6595
- });
6596
- return result as TOutput;
6597
- },
6608
+ this.inlineChildAggregates.total += 1;
6609
+ const inlineScalarChild =
6610
+ childExecutionDecision.reason === 'scalar_child';
6611
+ const childContext = createPlayContext({
6612
+ ...this.#options,
6613
+ executionScope: childExecutionScope,
6614
+ playId: resolvedName,
6615
+ playName: resolvedName,
6616
+ runId: compositionNamespace,
6617
+ executorToken: inlineScalarChild
6618
+ ? this.#options.executorToken
6619
+ : await this.mintChildExecutorToken({
6620
+ parentRunId: this.currentRunId,
6621
+ parentPlayName: this.#options.playName,
6622
+ childRunId: compositionNamespace,
6623
+ childPlayName: resolvedName,
6624
+ }),
6625
+ staticPipeline: resolvedPlay.staticPipeline ?? null,
6626
+ checkpoint: this.checkpoint,
6627
+ governance: undefined,
6628
+ executionGovernor: inlineChildGovernor!,
6629
+ ...(inlineScalarChild
6630
+ ? {}
6631
+ : {
6632
+ getRuntimeStepReceipt: undefined,
6633
+ getRuntimeStepReceipts: undefined,
6634
+ claimRuntimeStepReceipt: undefined,
6635
+ claimRuntimeStepReceipts: undefined,
6636
+ completeRuntimeStepReceipt: undefined,
6637
+ completeRuntimeStepReceipts: undefined,
6638
+ failRuntimeStepReceipt: undefined,
6639
+ failRuntimeStepReceipts: undefined,
6640
+ heartbeatRuntimeStepReceipts: undefined,
6641
+ skipRuntimeStepReceipt: undefined,
6642
+ }),
6598
6643
  });
6644
+ try {
6645
+ const childExecution = this.executeResolvedPlay(
6646
+ resolvedPlay,
6647
+ childContext,
6648
+ input,
6649
+ );
6650
+ await childContext.drainQueuedWork([childExecution]);
6651
+ const result = await childExecution;
6652
+ this.inlineChildAggregates.ok += 1;
6653
+ if (rowStore) {
6654
+ this.emitScopedFieldMetaUpdate({
6655
+ rowId: rowStore.rowId,
6656
+ key: rowStore.rowKey ?? null,
6657
+ tableNamespace: rowStore.tableNamespace ?? null,
6658
+ fieldName: rowStore.fieldName,
6659
+ status: 'completed',
6660
+ rowStatus: 'running',
6661
+ stage: resolvedName,
6662
+ provider: 'deepline_native',
6663
+ error: null,
6664
+ producer,
6665
+ dataPatch: {},
6666
+ });
6667
+ }
6668
+ this.recordPlayCallStep({
6669
+ playId: resolvedName,
6670
+ description: options?.description,
6671
+ nestedSteps: childContext.getSteps(),
6672
+ });
6673
+ return result as TOutput;
6674
+ } catch (childError) {
6675
+ this.recordInlineChildFailure(resolvedName, childError);
6676
+ throw childError;
6677
+ }
6599
6678
  } catch (error) {
6600
6679
  if (isPlayExecutionSuspendedError(error)) {
6601
6680
  throw error;
@@ -6695,65 +6774,91 @@ export class PlayContextImpl {
6695
6774
  return body.executorToken.trim();
6696
6775
  }
6697
6776
 
6698
- private async registerInlineChildRun(input: {
6699
- scope: RunExecutionScope;
6700
- staticPipeline: unknown;
6701
- }): Promise<void> {
6702
- const baseUrl = this.#options.baseUrl?.trim();
6703
- const executorToken = this.#options.executorToken?.trim();
6704
- if (!baseUrl || !executorToken) return;
6705
-
6706
- await startRunViaAppRuntime(
6707
- {
6708
- baseUrl,
6709
- executorToken,
6710
- integrationMode: this.#options.integrationMode ?? null,
6711
- vercelProtectionBypassToken:
6712
- this.#options.vercelProtectionBypassToken ?? null,
6713
- },
6714
- {
6715
- playName: input.scope.logical.playId,
6716
- runId: input.scope.logical.runId,
6717
- parentRunId: input.scope.logical.parentRunId,
6718
- rootRunId: input.scope.logical.rootRunId,
6719
- workflowFamilyKey: input.scope.logical.rootRunId,
6720
- artifactHash: this.#options.artifactHash ?? null,
6721
- graphHash: this.#options.graphHash ?? null,
6722
- schedulerSchema: this.#options.runtimeSchedulerSchema ?? null,
6723
- executionProfile: this.#options.childRunProfile ?? 'absurd',
6724
- staticPipeline: input.staticPipeline,
6725
- source: 'published',
6726
- },
6727
- );
6777
+ /**
6778
+ * Replay-stable composition namespace for one inline `ctx.runPlay`
6779
+ * invocation. Format `child:<playName>#<callKey>[@<rowScope>]`:
6780
+ *
6781
+ * - `callKey` is the author's normalized `ctx.runPlay(key, ...)` call key,
6782
+ * so distinct call sites never collide.
6783
+ * - `rowScope` (table namespace + row key) is appended when the invocation
6784
+ * runs inside a map/dataset row resolver, so the SAME call site invoked
6785
+ * across rows scopes each row's child step receipts separately.
6786
+ *
6787
+ * The namespace is derived purely from authored, content-stable inputs —
6788
+ * never from arrival order — so a retried run attempt reproduces the same
6789
+ * namespace under any row-completion interleaving and the child's tool
6790
+ * receipts (owned by the PARENT run) recover instead of re-billing. Inline
6791
+ * children carry NO durable run id — this namespace is their only identity.
6792
+ * See ADR 0013.
6793
+ */
6794
+ private inlineChildCompositionNamespace(
6795
+ childPlayName: string,
6796
+ normalizedKey: string,
6797
+ ): string {
6798
+ const rowScope = rowContext.getStore();
6799
+ const rowScopeKey = rowScope
6800
+ ? `${rowScope.tableNamespace ?? rowScope.fieldName ?? 'rows'}:${
6801
+ rowScope.rowKey ?? String(rowScope.rowId)
6802
+ }`
6803
+ : '';
6804
+ const namespace = `child:${childPlayName}#${normalizedKey}${
6805
+ rowScopeKey ? `@${rowScopeKey}` : ''
6806
+ }`;
6807
+ if (!this.inlineChildInvocationOrdinals.has(namespace)) {
6808
+ // Per-row fan-width guard. `maxPlayCallDepth` (governor) bounds nesting;
6809
+ // this bounds distinct inline invocations from ONE row scope so a
6810
+ // resolver looping runPlay unbounded fails loudly on that row instead of
6811
+ // leaking namespaces.
6812
+ const rowInvocationCount =
6813
+ (this.inlineChildInvocationsPerRow.get(rowScopeKey) ?? 0) + 1;
6814
+ if (rowInvocationCount > MAX_INLINE_CHILD_INVOCATIONS_PER_ROW) {
6815
+ throw new Error(
6816
+ `ctx.runPlay("${childPlayName}") exceeded the inline child invocation cap ` +
6817
+ `(${MAX_INLINE_CHILD_INVOCATIONS_PER_ROW} per row). A resolver is calling ` +
6818
+ 'runPlay in an unbounded loop; give the batch ONE dataset child at play level instead.',
6819
+ );
6820
+ }
6821
+ this.inlineChildInvocationsPerRow.set(rowScopeKey, rowInvocationCount);
6822
+ this.inlineChildInvocationOrdinals.set(
6823
+ namespace,
6824
+ this.inlineChildInvocationOrdinals.size,
6825
+ );
6826
+ }
6827
+ return namespace;
6728
6828
  }
6729
6829
 
6730
- private async settleInlineChildRun(input: {
6731
- scope: RunExecutionScope;
6732
- status: 'completed' | 'failed' | 'cancelled';
6733
- result?: unknown;
6734
- error?: string | null;
6735
- }): Promise<void> {
6736
- const baseUrl = this.#options.baseUrl?.trim();
6737
- const executorToken = this.#options.executorToken?.trim();
6738
- if (!baseUrl || !executorToken) return;
6830
+ private recordInlineChildFailure(
6831
+ childPlayName: string,
6832
+ error: unknown,
6833
+ ): void {
6834
+ this.inlineChildAggregates.failed += 1;
6835
+ if (
6836
+ this.inlineChildAggregates.failures.length <
6837
+ MAX_INLINE_CHILD_FAILURE_DETAIL
6838
+ ) {
6839
+ this.inlineChildAggregates.failures.push({
6840
+ childPlayName,
6841
+ error: this.formatRuntimeError(error),
6842
+ });
6843
+ }
6844
+ }
6739
6845
 
6740
- await settleInlineChildRunViaAppRuntime(
6741
- {
6742
- baseUrl,
6743
- executorToken,
6744
- integrationMode: this.#options.integrationMode ?? null,
6745
- vercelProtectionBypassToken:
6746
- this.#options.vercelProtectionBypassToken ?? null,
6747
- },
6748
- {
6749
- parentRunId: input.scope.logical.parentRunId ?? this.currentRunId,
6750
- childRunId: input.scope.logical.runId,
6751
- childPlayName: input.scope.logical.playId,
6752
- status: input.status,
6753
- ...(input.result !== undefined ? { result: input.result } : {}),
6754
- ...(input.error !== undefined ? { error: input.error } : {}),
6755
- },
6756
- );
6846
+ /**
6847
+ * Snapshot of inline child aggregates for the single-writer progress event.
6848
+ * Read-only; never mutates. Omitted entirely when no inline child has run so
6849
+ * plays without child composition keep byte-identical progress events.
6850
+ */
6851
+ private inlineChildAggregateEventFields(): {
6852
+ childrenTotal?: number;
6853
+ childrenOk?: number;
6854
+ childrenFailed?: number;
6855
+ } {
6856
+ if (this.inlineChildAggregates.total === 0) return {};
6857
+ return {
6858
+ childrenTotal: this.inlineChildAggregates.total,
6859
+ childrenOk: this.inlineChildAggregates.ok,
6860
+ childrenFailed: this.inlineChildAggregates.failed,
6861
+ };
6757
6862
  }
6758
6863
 
6759
6864
  /**
@@ -412,6 +412,14 @@ export type PlayExecutionEvent =
412
412
  completedRows: number;
413
413
  failedRows: number;
414
414
  totalRows?: number;
415
+ /**
416
+ * Inline child-play composition aggregates for this run. Maintained by the
417
+ * single-writer progress path (never per child), so fan-out cannot contend.
418
+ * See ADR 0013.
419
+ */
420
+ childrenTotal?: number;
421
+ childrenOk?: number;
422
+ childrenFailed?: number;
415
423
  at: number;
416
424
  }
417
425
  | {
@@ -436,6 +444,13 @@ export type PlayExecutionEvent =
436
444
  completedRows: number;
437
445
  failedRows: number;
438
446
  totalRows?: number;
447
+ /**
448
+ * Inline child-play composition aggregates for this run. Maintained by the
449
+ * single-writer progress path (never per child). See ADR 0013.
450
+ */
451
+ childrenTotal?: number;
452
+ childrenOk?: number;
453
+ childrenFailed?: number;
439
454
  at: number;
440
455
  };
441
456