deepline 0.3.128 → 0.3.130

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 (130) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +34 -9
  2. package/dist/bundling-sources/sdk/src/http.ts +22 -0
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +4 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
  13. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
  14. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
  18. package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
  23. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
  24. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
  25. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
  26. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
  28. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
  30. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
  31. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
  32. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
  33. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
  36. package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
  37. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
  43. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
  52. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
  53. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
  54. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
  55. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
  58. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
  59. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
  60. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
  61. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
  62. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
  63. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
  64. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
  67. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
  68. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
  70. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
  71. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +114 -4
  72. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
  73. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
  79. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
  82. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
  83. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
  84. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
  85. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
  86. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
  87. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
  88. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +28 -8
  89. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
  90. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
  91. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
  92. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +146 -15
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +904 -77
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
  103. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
  104. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
  105. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
  106. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
  109. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
  110. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
  111. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
  112. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
  113. package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
  114. package/dist/cli/index.js +250 -38
  115. package/dist/cli/index.mjs +250 -38
  116. package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
  117. package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
  118. package/dist/index.d.mts +4 -2
  119. package/dist/index.d.ts +4 -2
  120. package/dist/index.js +231 -35
  121. package/dist/index.mjs +231 -35
  122. package/dist/install-integrity.json +31 -2
  123. package/dist/plays/bundle-play-file.d.mts +2 -2
  124. package/dist/plays/bundle-play-file.d.ts +2 -2
  125. package/dist/plays/bundle-play-file.mjs +65 -1
  126. package/dist/release.d.mts +1 -1
  127. package/dist/release.d.ts +1 -1
  128. package/dist/release.js +1 -1
  129. package/dist/release.mjs +1 -1
  130. package/package.json +1 -1
@@ -18,14 +18,22 @@ import {
18
18
  runtimeReceiptCompletionResponse,
19
19
  type LegacyRuntimeReceiptWireFormat,
20
20
  } from '../runtime-contract';
21
+ import { BoundedBatchCapacityError } from '../bounded-batch-function';
21
22
  import type {
22
23
  RuntimeReceiptAction,
23
24
  RuntimeReceiptFailure,
24
25
  RuntimeReceiptLease,
25
26
  } from '../runtime-actions';
27
+ import type { RuntimeStepReceipt } from '../ctx-types';
28
+ import {
29
+ isRuntimeReceiptValueReference,
30
+ normalizeRuntimeReceiptValueInlineFields,
31
+ type RuntimeReceiptValueReference,
32
+ } from '../runtime-receipt-value';
26
33
  import {
27
34
  redactLedgerString,
28
35
  sanitizeRuntimeReceiptPayload,
36
+ sanitizeRuntimeReceiptPayloadForStorage,
29
37
  } from '../ledger-safe-payload';
30
38
  import {
31
39
  claimPostgresSchedulerReceipt,
@@ -53,6 +61,15 @@ import {
53
61
  import { transientServiceUnavailableDetail } from '../transient-service-error';
54
62
  import { isRuntimePostgresAdmissionCapacityError } from '../runtime-postgres-admission';
55
63
  import { normalizeToolExecutionFailure } from '../../plays/tool-execution-error';
64
+ import {
65
+ assertRuntimeReceiptValueReferenceScope,
66
+ createRuntimeReceiptValuePersister,
67
+ createRuntimeReceiptValueReader,
68
+ RUNTIME_RECEIPT_VALUE_INLINE_MAX_BYTES,
69
+ type RuntimeReceiptValueBlobStore,
70
+ type RuntimeReceiptValuePersister,
71
+ type RuntimeReceiptValueReader,
72
+ } from './runtime-receipt-value-storage';
56
73
 
57
74
  type RuntimeReceiptActionWithOptionalScope<
58
75
  TAction extends RuntimeReceiptAction = RuntimeReceiptAction,
@@ -82,6 +99,7 @@ export const RECEIPT_GATEWAY_ACTIONS = new Set<string>([
82
99
  'mark_runtime_step_receipts_queued',
83
100
  'complete_runtime_step_receipt',
84
101
  'complete_runtime_step_receipts',
102
+ 'complete_runtime_step_receipts_by_reference',
85
103
  'release_runtime_step_receipt',
86
104
  'fail_runtime_step_receipt',
87
105
  'fail_runtime_step_receipts',
@@ -213,6 +231,25 @@ export function receiptGatewayFailureResponse(
213
231
  },
214
232
  );
215
233
  }
234
+ if (error instanceof BoundedBatchCapacityError) {
235
+ return receiptGatewayJsonResponse(
236
+ {
237
+ error: `${operationLabel} is waiting for bounded batch capacity.`,
238
+ code: 'runtime_batch_backpressure',
239
+ retryable: true,
240
+ retry_after_ms: 1_000,
241
+ detail: error.message,
242
+ ...(context.action ? { action: context.action } : {}),
243
+ },
244
+ {
245
+ status: 503,
246
+ headers: {
247
+ 'retry-after': '1',
248
+ 'x-deepline-error-code': 'runtime_batch_backpressure',
249
+ },
250
+ },
251
+ );
252
+ }
216
253
  if (isRuntimeSchedulerCapacityError(error)) {
217
254
  const retryAfterMs = runtimeSchedulerCapacityRetryAfterMs(error);
218
255
  return receiptGatewayJsonResponse(
@@ -377,6 +414,216 @@ function receiptKeys(value: unknown): Array<string> | Response {
377
414
  return keys;
378
415
  }
379
416
 
417
+ /**
418
+ * Redact, serialize, and size-check one receipt output exactly once.
419
+ *
420
+ * `outputJson` is the canonical Postgres JSON text for `output`, so the
421
+ * completion statement binds it straight to `jsonb` instead of re-running
422
+ * `stringifyPostgresJson` over the same value. Serializing a 600 KiB result
423
+ * four times (gate, pre-redaction measure, post-redaction measure, parameter
424
+ * build) was roughly half of gateway CPU on `complete_runtime_step_receipts`.
425
+ */
426
+ async function receiptCompletionOutput(input: {
427
+ orgId: string;
428
+ key: string;
429
+ output: unknown;
430
+ inlineFields?: unknown;
431
+ persist: RuntimeReceiptValuePersister;
432
+ }): Promise<{
433
+ outputJson: string | null;
434
+ outputRef?: RuntimeReceiptValueReference;
435
+ /**
436
+ * Wall spent in the value persister after the value was redacted and
437
+ * serialized. For a referenced value this includes its bounded R2 admission
438
+ * wait and immutable PUT; for a small value it is the inline fast path.
439
+ *
440
+ * This is intentionally kept gateway-internal. The compact completion
441
+ * acknowledgement never carries storage timing or an object identity.
442
+ */
443
+ valuePersistMs: number;
444
+ valuePersistedByReference: boolean;
445
+ }> {
446
+ const sanitized = sanitizeRuntimeReceiptPayloadForStorage(
447
+ input.output ?? null,
448
+ );
449
+ const inlineFields =
450
+ input.inlineFields === undefined
451
+ ? undefined
452
+ : normalizeRuntimeReceiptValueInlineFields(
453
+ sanitizeRuntimeReceiptPayload(input.inlineFields),
454
+ );
455
+ const persistStartedAt = Date.now();
456
+ const stored = await input.persist({
457
+ orgId: input.orgId,
458
+ receiptKey: input.key,
459
+ serialized: sanitized.serialized,
460
+ inlineFields,
461
+ });
462
+ return {
463
+ outputJson: stored.outputJson,
464
+ ...(stored.kind === 'reference' ? { outputRef: stored.outputRef } : {}),
465
+ valuePersistMs: Math.max(0, Date.now() - persistStartedAt),
466
+ valuePersistedByReference: stored.kind === 'reference',
467
+ };
468
+ }
469
+
470
+ export type ReceiptGatewayValuePersistenceObservation = {
471
+ /** Number of receipt outputs in this HTTP completion request. */
472
+ values: number;
473
+ /** Outputs actually persisted through the bounded immutable-object writer. */
474
+ referencedValues: number;
475
+ /**
476
+ * Slowest individual persistence wall in this HTTP request. It includes the
477
+ * process-shared R2 writer's admission wait, so it is the useful value to
478
+ * compare with the gateway callback wall. It is zero only when every output
479
+ * took the inline path.
480
+ */
481
+ maxPersistMs: number;
482
+ };
483
+
484
+ type ReceiptCompletionOutputWithPersistence = Awaited<
485
+ ReturnType<typeof receiptCompletionOutput>
486
+ >;
487
+
488
+ /**
489
+ * Publish a deliberately best-effort view of the value-persistence portion of
490
+ * a completion. The ordinary and bulk completion actions share this helper so
491
+ * a legacy single receipt cannot make the gateway look faster than it is.
492
+ */
493
+ function observeReceiptValuePersistence(
494
+ outputs: readonly ReceiptCompletionOutputWithPersistence[],
495
+ observe?: (observation: ReceiptGatewayValuePersistenceObservation) => void,
496
+ ): void {
497
+ try {
498
+ observe?.({
499
+ values: outputs.length,
500
+ referencedValues: outputs.filter(
501
+ (value) => value.valuePersistedByReference,
502
+ ).length,
503
+ maxPersistMs: outputs.reduce(
504
+ (max, value) => Math.max(max, value.valuePersistMs),
505
+ 0,
506
+ ),
507
+ });
508
+ } catch {
509
+ // Observability is intentionally best-effort.
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Project one `complete_runtime_step_receipts` body into storage completions.
515
+ *
516
+ * Shared by the coalesced commit and the client-bound switch so both paths
517
+ * sanitize, serialize, and scope identically; `keys` are the already
518
+ * scope-checked receipt keys, in request order.
519
+ */
520
+ async function buildReceiptCompletions(input: {
521
+ orgId: string;
522
+ persist: RuntimeReceiptValuePersister;
523
+ receipts: readonly unknown[];
524
+ keys: readonly string[];
525
+ runId: string;
526
+ onValuePersistence?: (
527
+ observation: ReceiptGatewayValuePersistenceObservation,
528
+ ) => void;
529
+ }): Promise<ReceiptGatewayCompletionEntry[]> {
530
+ const built = await Promise.all(
531
+ (input.receipts as Array<Record<string, unknown>>).map(
532
+ async (receipt, index) => {
533
+ const output = await receiptCompletionOutput({
534
+ orgId: input.orgId,
535
+ key: input.keys[index]!,
536
+ output: receipt.output,
537
+ inlineFields: receipt.inlineFields,
538
+ persist: input.persist,
539
+ });
540
+ return {
541
+ key: input.keys[index]!,
542
+ runId: input.runId,
543
+ runAttempt:
544
+ typeof receipt.runAttempt === 'number' ? receipt.runAttempt : null,
545
+ leaseId: typeof receipt.leaseId === 'string' ? receipt.leaseId : null,
546
+ ...output,
547
+ };
548
+ },
549
+ ),
550
+ );
551
+ observeReceiptValuePersistence(built, input.onValuePersistence);
552
+ return built.map(
553
+ ({
554
+ valuePersistMs: _valuePersistMs,
555
+ valuePersistedByReference: _ref,
556
+ ...completion
557
+ }) => completion,
558
+ );
559
+ }
560
+
561
+ /**
562
+ * The execution relay writes a complete, serialized ToolExecuteResult to the
563
+ * receipt-owned immutable key before it attaches the reference to its normal
564
+ * provider response. This receiver validates that the reference belongs to
565
+ * the same authenticated receipt and commits only its compact manifest.
566
+ */
567
+ async function buildReferencedReceiptCompletions(input: {
568
+ orgId: string;
569
+ receipts: readonly unknown[];
570
+ keys: readonly string[];
571
+ runId: string;
572
+ store?: RuntimeReceiptValueBlobStore;
573
+ }): Promise<ReceiptGatewayCompletionEntry[]> {
574
+ return await Promise.all(
575
+ (input.receipts as Array<Record<string, unknown>>).map(
576
+ async (receipt, index) => {
577
+ const outputRef = receipt.outputRef;
578
+ if (!isRuntimeReceiptValueReference(outputRef)) {
579
+ throw new Error(
580
+ 'Referenced receipt completion requires a valid outputRef.',
581
+ );
582
+ }
583
+ const key = input.keys[index]!;
584
+ assertRuntimeReceiptValueReferenceScope({
585
+ orgId: input.orgId,
586
+ receiptKey: key,
587
+ reference: outputRef,
588
+ });
589
+ // A reference must not point at an object a later receipt read cannot
590
+ // redeem. The execution relay has already recovered an ambiguous PUT
591
+ // before sending it, so this is a cheap integrity fence, not a second
592
+ // provider-side operation.
593
+ if (input.store && !(await input.store.exists(outputRef.key))) {
594
+ throw new Error(
595
+ 'Referenced receipt output is not present in immutable storage.',
596
+ );
597
+ }
598
+ return {
599
+ key,
600
+ runId: input.runId,
601
+ runAttempt:
602
+ typeof receipt.runAttempt === 'number' ? receipt.runAttempt : null,
603
+ leaseId: typeof receipt.leaseId === 'string' ? receipt.leaseId : null,
604
+ outputJson: null,
605
+ outputRef,
606
+ };
607
+ },
608
+ ),
609
+ );
610
+ }
611
+
612
+ /** Serialized completion size, used only for the coalescer's byte bound. */
613
+ function receiptCompletionBytes(
614
+ completions: readonly ReceiptGatewayCompletionEntry[],
615
+ ): number {
616
+ let bytes = 0;
617
+ for (const completion of completions) {
618
+ const storedPayload =
619
+ completion.outputJson ?? JSON.stringify(completion.outputRef ?? null);
620
+ bytes +=
621
+ Buffer.byteLength(completion.key, 'utf8') +
622
+ Buffer.byteLength(storedPayload, 'utf8');
623
+ }
624
+ return bytes;
625
+ }
626
+
380
627
  export type ReceiptGatewayDispatcher = (
381
628
  scope: ReceiptGatewayScope,
382
629
  body: unknown,
@@ -397,6 +644,15 @@ export type ReceiptGatewayDispatchOptions = {
397
644
  receipts: readonly unknown[];
398
645
  forceRefresh: boolean;
399
646
  }) => void;
647
+ /**
648
+ * On-the-wire body bytes of the HTTP request being dispatched, when the
649
+ * transport knows them and they differ from the decoded size — that is, when
650
+ * the body arrived gzipped. Telemetry only: it is forwarded to the completion
651
+ * commit so a coalesced statement can report what the network actually moved
652
+ * beside the decoded bytes it was admitted on. Absent (uncompressed request,
653
+ * PGlite, tests) the commit's wire bytes default to its decoded estimate.
654
+ */
655
+ requestWireBytes?: number | null;
400
656
  };
401
657
 
402
658
  async function injectReceiptCompletionFailure(input: {
@@ -436,13 +692,152 @@ async function injectReceiptCompletionFailure(input: {
436
692
  }
437
693
  }
438
694
 
695
+ /**
696
+ * One receipt's completion, already scope-checked and serialized once by the
697
+ * gateway. It carries either canonical inline `jsonb` text or a compact,
698
+ * already-published value reference, so a coalescing commit never re-walks a
699
+ * provider body.
700
+ */
701
+ export type ReceiptGatewayCompletionEntry = {
702
+ key: string;
703
+ runId: string;
704
+ runAttempt: number | null;
705
+ leaseId: string | null;
706
+ outputJson: string | null;
707
+ outputRef?: RuntimeReceiptValueReference;
708
+ };
709
+
439
710
  export function createReceiptGatewayDispatcher(input: {
440
711
  withClient: <T>(
441
712
  fn: (client: RuntimeSchedulerQueryClient) => Promise<T>,
442
713
  ) => Promise<T>;
443
714
  schema?: string;
715
+ /** Test seam and the Fly value-store adapter for large receipt bodies. */
716
+ runtimeReceiptValueStore?: RuntimeReceiptValueBlobStore;
717
+ runtimeReceiptValueInlineMaxBytes?: number;
718
+ /**
719
+ * The Fly reader-fence adapter enables this only after every durable gateway
720
+ * reader proves it understands referenced values. Tests and local adapters
721
+ * retain their explicit injected-store behavior when this is omitted.
722
+ */
723
+ runtimeReceiptValueReferencesEnabled?: boolean;
724
+ runtimeReceiptValuePersister?: RuntimeReceiptValuePersister;
725
+ runtimeReceiptValueReader?: RuntimeReceiptValueReader;
726
+ /**
727
+ * Optional gateway-local observation of large receipt-value persistence.
728
+ * It is diagnostic-only: a slow or throwing observer must not affect the
729
+ * durable completion it observes.
730
+ */
731
+ onReceiptValuePersistence?: (
732
+ observation: ReceiptGatewayValuePersistenceObservation,
733
+ ) => void;
444
734
  consumeReceiptCompleteFault?: (runId: string) => boolean | Promise<boolean>;
735
+ /**
736
+ * Commit bulk receipt completions outside this dispatcher's own client.
737
+ *
738
+ * The production gateway supplies a coalescing commit that merges concurrent
739
+ * requests of the same org/schema/wire format into one statement under one
740
+ * heavy-admission slot. It MUST return results index-aligned with the
741
+ * supplied receipts, and MUST throw the error the caller would have seen for
742
+ * an uncoalesced request so every merged request stays retry-safe. Absent
743
+ * (tests, PGlite), completions run through `withClient` exactly as before.
744
+ */
745
+ commitReceiptCompletions?: (commit: {
746
+ orgId: string;
747
+ schema: string | null;
748
+ wireFormat: LegacyRuntimeReceiptWireFormat;
749
+ receipts: ReceiptGatewayCompletionEntry[];
750
+ bytes: number;
751
+ /**
752
+ * On-the-wire body bytes of the request that carried these receipts.
753
+ * Equals `bytes` when the request arrived uncompressed. Telemetry only.
754
+ */
755
+ wireBytes: number;
756
+ }) => Promise<Array<RuntimeStepReceipt | null>>;
445
757
  }): ReceiptGatewayDispatcher {
758
+ const inlineMaxBytes =
759
+ input.runtimeReceiptValueInlineMaxBytes ??
760
+ RUNTIME_RECEIPT_VALUE_INLINE_MAX_BYTES;
761
+ const persistReceiptValue: RuntimeReceiptValuePersister =
762
+ input.runtimeReceiptValueReferencesEnabled === false
763
+ ? async (value) => ({ kind: 'inline', outputJson: value.serialized })
764
+ : (input.runtimeReceiptValuePersister ??
765
+ (input.runtimeReceiptValueStore
766
+ ? createRuntimeReceiptValuePersister({
767
+ store: input.runtimeReceiptValueStore,
768
+ inlineMaxBytes,
769
+ })
770
+ : async (value) => {
771
+ // Expand readers ship independently from writers. Test/local dispatchers
772
+ // remain inline until their Fly host explicitly supplies the authenticated
773
+ // value store; a reference is never published merely because a process
774
+ // happens to have ambient R2 credentials.
775
+ return { kind: 'inline', outputJson: value.serialized };
776
+ }));
777
+
778
+ const readReceiptValue: RuntimeReceiptValueReader =
779
+ input.runtimeReceiptValueReader ??
780
+ createRuntimeReceiptValueReader(
781
+ input.runtimeReceiptValueStore
782
+ ? { store: input.runtimeReceiptValueStore }
783
+ : {},
784
+ );
785
+ const hydrateReceipt = async (
786
+ receipt: RuntimeStepReceipt | null,
787
+ orgId: string,
788
+ ): Promise<RuntimeStepReceipt | null> => {
789
+ if (!receipt?.outputRef) return receipt;
790
+ const output = await readReceiptValue({
791
+ orgId,
792
+ receiptKey: receipt.key,
793
+ reference: receipt.outputRef,
794
+ });
795
+ const { outputRef: _outputRef, ...hydrated } = receipt;
796
+ return {
797
+ ...hydrated,
798
+ ...(receipt.outputRef.inlineFields
799
+ ? { inlineFields: receipt.outputRef.inlineFields }
800
+ : {}),
801
+ output,
802
+ };
803
+ };
804
+ /**
805
+ * A completed-cache probe needs terminal state and a safe byte price, not a
806
+ * second copy of the result. Keep R2 references gateway-internal: only the
807
+ * scalar size crosses to the runner, and a later normal read still redeems
808
+ * and validates the body before it can become a cache hit.
809
+ */
810
+ const receiptManifest = (
811
+ receipt: RuntimeStepReceipt | null,
812
+ ): RuntimeStepReceipt | null => {
813
+ if (!receipt) return null;
814
+ const { output, outputRef, ...manifest } = receipt;
815
+ const outputByteLength =
816
+ outputRef?.byteLength ??
817
+ (output === undefined
818
+ ? null
819
+ : new TextEncoder().encode(JSON.stringify(output)).byteLength);
820
+ return {
821
+ ...manifest,
822
+ ...(outputByteLength === null ? {} : { outputByteLength }),
823
+ };
824
+ };
825
+ const hydrateReceipts = async (
826
+ receipts: readonly (RuntimeStepReceipt | null)[],
827
+ orgId: string,
828
+ ): Promise<Array<RuntimeStepReceipt | null>> =>
829
+ await Promise.all(
830
+ receipts.map(async (receipt) => await hydrateReceipt(receipt, orgId)),
831
+ );
832
+ const presentCompletionReceipt = async (input: {
833
+ receipt: RuntimeStepReceipt | null;
834
+ orgId: string;
835
+ wireFormat: LegacyRuntimeReceiptWireFormat;
836
+ }): Promise<RuntimeStepReceipt | null> =>
837
+ input.wireFormat < 3
838
+ ? await hydrateReceipt(input.receipt, input.orgId)
839
+ : input.receipt;
840
+
446
841
  return async (scope, rawBody, dispatchOptions = {}) => {
447
842
  if (!rawBody || typeof rawBody !== 'object') {
448
843
  return receiptGatewayJsonResponse(
@@ -454,6 +849,11 @@ export function createReceiptGatewayDispatcher(input: {
454
849
  const scopeError = requireReceiptScope(scope, body);
455
850
  if (scopeError) return scopeError;
456
851
  const runId = String(body.runId || scope.runId);
852
+ // Additive read presentation. Older callers omit this and receive the
853
+ // exact hydrated body they did before; only a Foundation cache probe asks
854
+ // for the compact manifest.
855
+ const hydrateOutput =
856
+ !('hydrateOutput' in body) || body.hydrateOutput !== false;
457
857
  const wireFormat = dispatchOptions.wireFormat ?? 3;
458
858
  // Org scope is proven by the executor token at the transport (production:
459
859
  // apps/runtime-receipt-gateway/index.ts passes auth.orgId here). The
@@ -461,6 +861,67 @@ export function createReceiptGatewayDispatcher(input: {
461
861
  // through identically for the test/PGlite dispatch to match production.
462
862
  const options = { schema: input.schema, orgId: scope.orgId };
463
863
 
864
+ // A coalescing commit owns admission, the pooled client, and the statement
865
+ // for a whole window of requests, so this path must not take a client of
866
+ // its own — that is the entire cost being removed. The injected
867
+ // `receipt_complete_write_fail` path still needs one, so it falls through
868
+ // to the client-bound switch with its fault decision already consumed.
869
+ let completionFaultInjected: boolean | null = null;
870
+ if (
871
+ body.action === 'complete_runtime_step_receipts' &&
872
+ input.commitReceiptCompletions
873
+ ) {
874
+ const sameRunError = requireSameRunReceipts(runId, body.receipts);
875
+ if (sameRunError) return sameRunError;
876
+ const keys = receiptKeys(
877
+ (body.receipts as Array<Record<string, unknown>>).map(
878
+ (receipt) => receipt.key,
879
+ ),
880
+ );
881
+ if (keys instanceof Response) return keys;
882
+ completionFaultInjected =
883
+ (await input.consumeReceiptCompleteFault?.(runId)) === true;
884
+ if (!completionFaultInjected) {
885
+ const completions = await buildReceiptCompletions({
886
+ orgId: scope.orgId,
887
+ persist: persistReceiptValue,
888
+ receipts: body.receipts,
889
+ keys,
890
+ runId,
891
+ onValuePersistence: input.onReceiptValuePersistence,
892
+ });
893
+ const bytes = receiptCompletionBytes(completions);
894
+ const committed = await input.commitReceiptCompletions({
895
+ orgId: scope.orgId,
896
+ schema: input.schema ?? null,
897
+ wireFormat,
898
+ receipts: completions,
899
+ bytes,
900
+ // A transport that did not report a wire size sent these bytes
901
+ // uncompressed, so the decoded estimate IS the wire cost.
902
+ wireBytes:
903
+ typeof dispatchOptions.requestWireBytes === 'number' &&
904
+ Number.isFinite(dispatchOptions.requestWireBytes)
905
+ ? Math.max(0, Math.floor(dispatchOptions.requestWireBytes))
906
+ : bytes,
907
+ });
908
+ return receiptGatewayJsonResponse(
909
+ await Promise.all(
910
+ committed.map(async (receipt) =>
911
+ runtimeReceiptCompletionResponse(
912
+ wireFormat,
913
+ await presentCompletionReceipt({
914
+ receipt,
915
+ orgId: scope.orgId,
916
+ wireFormat,
917
+ }),
918
+ ),
919
+ ),
920
+ ),
921
+ );
922
+ }
923
+ }
924
+
464
925
  return await input.withClient(async (client) => {
465
926
  switch (body.action) {
466
927
  case 'get_runtime_step_receipt': {
@@ -478,7 +939,11 @@ export function createReceiptGatewayDispatcher(input: {
478
939
  keys: [key],
479
940
  receipts: [receipt],
480
941
  });
481
- return receiptGatewayJsonResponse(receipt);
942
+ return receiptGatewayJsonResponse(
943
+ hydrateOutput
944
+ ? await hydrateReceipt(receipt, scope.orgId)
945
+ : receiptManifest(receipt),
946
+ );
482
947
  }
483
948
  case 'get_runtime_step_receipts': {
484
949
  const keys = receiptKeys(body.keys);
@@ -495,7 +960,11 @@ export function createReceiptGatewayDispatcher(input: {
495
960
  keys,
496
961
  receipts,
497
962
  });
498
- return receiptGatewayJsonResponse(receipts);
963
+ return receiptGatewayJsonResponse(
964
+ hydrateOutput
965
+ ? await hydrateReceipts(receipts, scope.orgId)
966
+ : receipts.map(receiptManifest),
967
+ );
499
968
  }
500
969
  case 'claim_runtime_step_receipt': {
501
970
  const key = receiptKey(body.key);
@@ -524,7 +993,11 @@ export function createReceiptGatewayDispatcher(input: {
524
993
  receipts: [receipt],
525
994
  forceRefresh: body.forceRefresh === true,
526
995
  });
527
- return receiptGatewayJsonResponse(receipt);
996
+ return receiptGatewayJsonResponse(
997
+ hydrateOutput
998
+ ? await hydrateReceipt(receipt, scope.orgId)
999
+ : receiptManifest(receipt),
1000
+ );
528
1001
  }
529
1002
  case 'claim_runtime_step_receipts': {
530
1003
  const keys = receiptKeys(body.keys);
@@ -553,7 +1026,11 @@ export function createReceiptGatewayDispatcher(input: {
553
1026
  receipts,
554
1027
  forceRefresh: body.forceRefresh === true,
555
1028
  });
556
- return receiptGatewayJsonResponse(receipts);
1029
+ return receiptGatewayJsonResponse(
1030
+ hydrateOutput
1031
+ ? await hydrateReceipts(receipts, scope.orgId)
1032
+ : receipts.map(receiptManifest),
1033
+ );
557
1034
  }
558
1035
  case 'mark_runtime_step_receipts_running': {
559
1036
  const sameRunError = requireSameRunReceipts(runId, body.receipts);
@@ -597,25 +1074,43 @@ export function createReceiptGatewayDispatcher(input: {
597
1074
  { status: 409 },
598
1075
  );
599
1076
  }
1077
+ const completionOutput = await receiptCompletionOutput({
1078
+ orgId: scope.orgId,
1079
+ key,
1080
+ output: body.output,
1081
+ inlineFields: body.inlineFields,
1082
+ persist: persistReceiptValue,
1083
+ });
1084
+ observeReceiptValuePersistence(
1085
+ [completionOutput],
1086
+ input.onReceiptValuePersistence,
1087
+ );
1088
+ const {
1089
+ valuePersistMs: _valuePersistMs,
1090
+ valuePersistedByReference: _valuePersistedByReference,
1091
+ ...storedCompletionOutput
1092
+ } = completionOutput;
1093
+ const completed = await completePostgresSchedulerReceipt(
1094
+ client,
1095
+ {
1096
+ key,
1097
+ runId,
1098
+ runAttempt:
1099
+ typeof body.runAttempt === 'number' ? body.runAttempt : null,
1100
+ leaseId: typeof body.leaseId === 'string' ? body.leaseId : null,
1101
+ ...storedCompletionOutput,
1102
+ returnOutput: wireFormat < 3,
1103
+ },
1104
+ options,
1105
+ );
600
1106
  return receiptGatewayJsonResponse(
601
1107
  runtimeReceiptCompletionResponse(
602
1108
  wireFormat,
603
- await completePostgresSchedulerReceipt(
604
- client,
605
- {
606
- key,
607
- runId,
608
- runAttempt:
609
- typeof body.runAttempt === 'number'
610
- ? body.runAttempt
611
- : null,
612
- leaseId:
613
- typeof body.leaseId === 'string' ? body.leaseId : null,
614
- output: sanitizeRuntimeReceiptPayload(body.output ?? null),
615
- returnOutput: wireFormat < 3,
616
- },
617
- options,
618
- ),
1109
+ await presentCompletionReceipt({
1110
+ receipt: completed,
1111
+ orgId: scope.orgId,
1112
+ wireFormat,
1113
+ }),
619
1114
  ),
620
1115
  );
621
1116
  }
@@ -628,18 +1123,18 @@ export function createReceiptGatewayDispatcher(input: {
628
1123
  ),
629
1124
  );
630
1125
  if (keys instanceof Response) return keys;
631
- const completions = body.receipts.map((receipt, index) => ({
632
- key: keys[index]!,
1126
+ const completions = await buildReceiptCompletions({
1127
+ orgId: scope.orgId,
1128
+ persist: persistReceiptValue,
1129
+ receipts: body.receipts,
1130
+ keys,
633
1131
  runId,
634
- runAttempt:
635
- typeof receipt.runAttempt === 'number'
636
- ? receipt.runAttempt
637
- : null,
638
- leaseId:
639
- typeof receipt.leaseId === 'string' ? receipt.leaseId : null,
640
- output: sanitizeRuntimeReceiptPayload(receipt.output ?? null),
641
- }));
642
- if (await input.consumeReceiptCompleteFault?.(runId)) {
1132
+ onValuePersistence: input.onReceiptValuePersistence,
1133
+ });
1134
+ if (
1135
+ completionFaultInjected ??
1136
+ (await input.consumeReceiptCompleteFault?.(runId))
1137
+ ) {
643
1138
  const [skippedReceipt, ...siblingReceipts] = completions;
644
1139
  if (siblingReceipts.length > 0) {
645
1140
  await completePostgresSchedulerReceipts(
@@ -669,18 +1164,82 @@ export function createReceiptGatewayDispatcher(input: {
669
1164
  { status: 409 },
670
1165
  );
671
1166
  }
1167
+ const completed = await completePostgresSchedulerReceipts(
1168
+ client,
1169
+ {
1170
+ receipts: completions,
1171
+ returnOutput: wireFormat < 3,
1172
+ },
1173
+ options,
1174
+ );
1175
+ return receiptGatewayJsonResponse(
1176
+ await Promise.all(
1177
+ completed.map(async (receipt) =>
1178
+ runtimeReceiptCompletionResponse(
1179
+ wireFormat,
1180
+ await presentCompletionReceipt({
1181
+ receipt,
1182
+ orgId: scope.orgId,
1183
+ wireFormat,
1184
+ }),
1185
+ ),
1186
+ ),
1187
+ ),
1188
+ );
1189
+ }
1190
+ case 'complete_runtime_step_receipts_by_reference': {
1191
+ // This action is the writer half of an expand/enable rollout. A
1192
+ // runner never relies on it without retaining its full live result,
1193
+ // so an unavailable receiver safely falls back to the legacy action
1194
+ // rather than turning a receipt reference into a null output.
1195
+ if (
1196
+ input.runtimeReceiptValueReferencesEnabled === false ||
1197
+ !input.runtimeReceiptValueStore
1198
+ ) {
1199
+ return receiptGatewayJsonResponse(
1200
+ {
1201
+ error:
1202
+ 'Receipt-output reference completion is not enabled on this gateway.',
1203
+ code: 'receipt_output_reference_unsupported',
1204
+ },
1205
+ { status: 409 },
1206
+ );
1207
+ }
1208
+ const sameRunError = requireSameRunReceipts(runId, body.receipts);
1209
+ if (sameRunError) return sameRunError;
1210
+ const keys = receiptKeys(
1211
+ (body.receipts as Array<Record<string, unknown>>).map(
1212
+ (receipt) => receipt.key,
1213
+ ),
1214
+ );
1215
+ if (keys instanceof Response) return keys;
1216
+ const completions = await buildReferencedReceiptCompletions({
1217
+ orgId: scope.orgId,
1218
+ receipts: body.receipts,
1219
+ keys,
1220
+ runId,
1221
+ store: input.runtimeReceiptValueStore,
1222
+ });
1223
+ const completed = await completePostgresSchedulerReceipts(
1224
+ client,
1225
+ {
1226
+ receipts: completions,
1227
+ returnOutput: wireFormat < 3,
1228
+ },
1229
+ options,
1230
+ );
672
1231
  return receiptGatewayJsonResponse(
673
- (
674
- await completePostgresSchedulerReceipts(
675
- client,
676
- {
677
- receipts: completions,
678
- returnOutput: wireFormat < 3,
679
- },
680
- options,
681
- )
682
- ).map((receipt) =>
683
- runtimeReceiptCompletionResponse(wireFormat, receipt),
1232
+ await Promise.all(
1233
+ completed.map(async (receipt) =>
1234
+ runtimeReceiptCompletionResponse(
1235
+ wireFormat,
1236
+ await presentCompletionReceipt({
1237
+ receipt,
1238
+ orgId: scope.orgId,
1239
+ wireFormat,
1240
+ }),
1241
+ ),
1242
+ ),
684
1243
  ),
685
1244
  );
686
1245
  }