deepline 0.3.128 → 0.3.129

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 (128) 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/deployment/commands/roll-execution-relay.ts +7 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
  17. package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
  23. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
  24. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
  25. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
  27. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
  30. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
  32. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
  35. package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
  42. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
  52. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
  53. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
  57. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
  58. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
  59. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
  61. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
  62. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
  63. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
  64. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
  67. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
  70. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
  71. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
  72. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
  73. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
  79. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
  82. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
  83. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
  84. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
  85. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
  86. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
  87. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
  89. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
  90. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
  103. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
  104. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
  106. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
  109. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
  110. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
  111. package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
  112. package/dist/cli/index.js +250 -38
  113. package/dist/cli/index.mjs +250 -38
  114. package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
  115. package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
  116. package/dist/index.d.mts +4 -2
  117. package/dist/index.d.ts +4 -2
  118. package/dist/index.js +231 -35
  119. package/dist/index.mjs +231 -35
  120. package/dist/install-integrity.json +31 -2
  121. package/dist/plays/bundle-play-file.d.mts +2 -2
  122. package/dist/plays/bundle-play-file.d.ts +2 -2
  123. package/dist/plays/bundle-play-file.mjs +65 -1
  124. package/dist/release.d.mts +1 -1
  125. package/dist/release.d.ts +1 -1
  126. package/dist/release.js +1 -1
  127. package/dist/release.mjs +1 -1
  128. package/package.json +1 -1
@@ -114,6 +114,7 @@ import {
114
114
  import {
115
115
  normalizePlayRuntimeEnvironment,
116
116
  normalizePlayRuntimeNamespace,
117
+ normalizePlayRuntimeSandboxPlacementPolicy,
117
118
  normalizePlayRuntimeSelection,
118
119
  type PlayRuntimeSelection,
119
120
  } from '../../shared_libs/play-runtime/runtime-environment.js';
@@ -304,7 +305,7 @@ function resolvePlayRunRuntimeSelection(
304
305
  const runtime = normalizePlayRuntimeSelection(request.runtime);
305
306
  if (!runtime) {
306
307
  throw new DeeplineError(
307
- 'runtime must be { environment: "preview", namespace, backend?: "daytona" | "modal" } with namespace matching ^[a-z][a-z0-9-]{0,30}$.',
308
+ 'runtime must be { environment: "preview", namespace, backend?: "daytona" | "modal", sandboxPlacementPolicy?: "policy_name@1" } with namespace matching ^[a-z][a-z0-9-]{0,30}$.',
308
309
  undefined,
309
310
  'INVALID_RUNTIME_SELECTION',
310
311
  );
@@ -320,8 +321,10 @@ function resolvePlayRunRuntimeSelection(
320
321
  const configuredNamespace = process.env.DEEPLINE_RUNTIME_NAMESPACE?.trim();
321
322
  const configuredToken =
322
323
  process.env.DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN?.trim();
324
+ const configuredPlacementPolicy =
325
+ process.env.DEEPLINE_RUNTIME_SANDBOX_PLACEMENT_POLICY?.trim();
323
326
  if (!configured) {
324
- if (configuredNamespace || configuredToken) {
327
+ if (configuredNamespace || configuredToken || configuredPlacementPolicy) {
325
328
  // Name the variables that are actually set. This branch is almost always
326
329
  // hit by a stale value auto-loaded from a .env/.env.local rather than by
327
330
  // a deliberate export, so the message has to say where to look and how
@@ -329,6 +332,9 @@ function resolvePlayRunRuntimeSelection(
329
332
  const present: string[] = [];
330
333
  if (configuredNamespace) present.push('DEEPLINE_RUNTIME_NAMESPACE');
331
334
  if (configuredToken) present.push('DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN');
335
+ if (configuredPlacementPolicy) {
336
+ present.push('DEEPLINE_RUNTIME_SANDBOX_PLACEMENT_POLICY');
337
+ }
332
338
  const isPlural = present.length > 1;
333
339
  throw new DeeplineError(
334
340
  [
@@ -359,18 +365,37 @@ function resolvePlayRunRuntimeSelection(
359
365
  'INVALID_RUNTIME_NAMESPACE',
360
366
  );
361
367
  }
362
- const configuredBackend = process.env.DEEPLINE_PLAY_RUNNER_BACKEND?.trim();
363
- if (!configuredBackend) {
364
- return { environment, namespace };
365
- }
366
- if (configuredBackend !== 'daytona' && configuredBackend !== 'modal') {
368
+ const configuredBackendValue =
369
+ process.env.DEEPLINE_PLAY_RUNNER_BACKEND?.trim();
370
+ const configuredBackend =
371
+ configuredBackendValue === 'daytona' || configuredBackendValue === 'modal'
372
+ ? configuredBackendValue
373
+ : undefined;
374
+ if (configuredBackendValue && !configuredBackend) {
367
375
  throw new DeeplineError(
368
- `DEEPLINE_PLAY_RUNNER_BACKEND must be daytona or modal for preview runtime selection. Received "${configuredBackend}".`,
376
+ `DEEPLINE_PLAY_RUNNER_BACKEND must be daytona or modal for preview runtime selection. Received "${configuredBackendValue}".`,
369
377
  undefined,
370
378
  'INVALID_RUNTIME_BACKEND',
371
379
  );
372
380
  }
373
- return { environment, namespace, backend: configuredBackend };
381
+ if (
382
+ configuredPlacementPolicy &&
383
+ !normalizePlayRuntimeSandboxPlacementPolicy(configuredPlacementPolicy)
384
+ ) {
385
+ throw new DeeplineError(
386
+ `DEEPLINE_RUNTIME_SANDBOX_PLACEMENT_POLICY must be a versioned placement policy id such as daytona_only@1. Received "${configuredPlacementPolicy}".`,
387
+ undefined,
388
+ 'INVALID_RUNTIME_SANDBOX_PLACEMENT_POLICY',
389
+ );
390
+ }
391
+ return {
392
+ environment,
393
+ namespace,
394
+ ...(configuredBackend ? { backend: configuredBackend } : {}),
395
+ ...(configuredPlacementPolicy
396
+ ? { sandboxPlacementPolicy: configuredPlacementPolicy }
397
+ : {}),
398
+ };
374
399
  }
375
400
 
376
401
  function runtimeSelectionHeaders(
@@ -550,6 +550,28 @@ export class HttpClient {
550
550
  throw lastError;
551
551
  }
552
552
 
553
+ // Vercel's deployment-protection interstitial is served as a 2xx
554
+ // HTML page, so it never reaches the !ok branch above. Without this
555
+ // the caller receives a string where it expected JSON and reports
556
+ // whatever field it found missing — an expired `vercel login` shows
557
+ // up as an unrelated validation error several layers away.
558
+ if (typeof parsed === 'string') {
559
+ const interstitial = detectHtmlErrorBody(
560
+ body,
561
+ response.headers.get('content-type'),
562
+ );
563
+ if (interstitial) {
564
+ throw new DeeplineError(
565
+ `${this.config.baseUrl} returned a deployment-protection page ` +
566
+ 'instead of JSON. Set VERCEL_PROTECTION_BYPASS_TOKEN, or run ' +
567
+ '`vercel login` if the CLI session expired.',
568
+ response.status,
569
+ 'DEPLOYMENT_PROTECTED',
570
+ { htmlErrorPage: true },
571
+ );
572
+ }
573
+ }
574
+
553
575
  return parsed as T;
554
576
  } catch (error) {
555
577
  clearTimeout(timeoutId);
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.128',
203
+ version: '0.3.129',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -3,6 +3,7 @@ import {
3
3
  PutObjectCommand,
4
4
  GetObjectCommand,
5
5
  HeadObjectCommand,
6
+ DeleteObjectCommand,
6
7
  } from '@aws-sdk/client-s3';
7
8
  import { NodeHttpHandler } from '@smithy/node-http-handler';
8
9
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@@ -203,6 +204,27 @@ export async function getBufferFromR2(
203
204
  return Buffer.from(bytes);
204
205
  }
205
206
 
207
+ /**
208
+ * Delete one immutable runtime object. Callers must retain their own durable
209
+ * deletion intent and confirm absence with HEAD before they acknowledge this
210
+ * operation: object storage and Postgres do not share a transaction.
211
+ */
212
+ export async function deleteR2Object(
213
+ key: string,
214
+ kind: R2BucketKind = 'default',
215
+ ): Promise<void> {
216
+ const startedAt = Date.now();
217
+ const config = getR2Config(kind);
218
+ const client = getR2Client(config);
219
+ await client.send(
220
+ new DeleteObjectCommand({
221
+ Bucket: config.bucket,
222
+ Key: key,
223
+ }),
224
+ );
225
+ logR2Perf('delete', key, startedAt, { kind });
226
+ }
227
+
206
228
  export async function createSignedR2ReadUrl(input: {
207
229
  key: string;
208
230
  kind?: R2BucketKind;
@@ -47,6 +47,14 @@ import {
47
47
  LEGACY_RUNTIME_RECEIPT_WIRE_HEADER,
48
48
  } from './/runtime-contract';
49
49
  import { PLAY_RUNTIME_API_COMPAT_PATH } from './/runtime-api-paths';
50
+ import {
51
+ RUNTIME_API_ACCEPTS_HEADER,
52
+ gzipRuntimeApiRequestBody,
53
+ isLegacyRuntimeApiGzipDecoderFailure,
54
+ noteRuntimeApiCompressionCapability,
55
+ revokeRuntimeApiCompressionCapability,
56
+ runtimeApiOriginAcceptsGzip,
57
+ } from './/runtime-api-compression';
50
58
  import {
51
59
  PLAY_RUNTIME_TEST_FAULT_HEADER,
52
60
  recognizedRuntimeTestFaultCount,
@@ -302,9 +310,143 @@ export type WorkerRuntimeApiContext = {
302
310
  requestTimeoutMs?: number | null;
303
311
  /** A higher-level writer owns retries and preserves the exact request body. */
304
312
  retryPolicy?: 'default' | 'none';
313
+ /**
314
+ * Observe the gateway request identity of a SUCCESSFUL response. Failures
315
+ * already carry it on the thrown error; a success carried it nowhere, so a
316
+ * response the caller found surprising could not be traced back to the
317
+ * gateway record that produced it.
318
+ */
319
+ onResponseMeta?: (meta: RuntimeApiResponseMeta) => void;
305
320
  signal?: AbortSignal;
306
321
  };
307
322
 
323
+ /**
324
+ * The receipt gateway's own account of a request, from its `Server-Timing`
325
+ * response header. Its `runtime.gateway.action.finished` durationMs starts
326
+ * AFTER authentication, so auth, body parse, and pool wait are invisible in
327
+ * gateway logs and observable only here — against `roundTripMs`, the client's
328
+ * measured wall for the same request, which is the only number that includes
329
+ * the network.
330
+ */
331
+ export type RuntimeGatewayServerTiming = {
332
+ authLaunchMs: number | null;
333
+ authVerifyMs: number | null;
334
+ bodyParseMs: number | null;
335
+ /**
336
+ * Gunzip of a compressed request body, inside `bodyParseMs`. Null when the
337
+ * request was not compressed or the gateway predates the entry.
338
+ */
339
+ bodyInflateMs: number | null;
340
+ dispatchPoolMs: number | null;
341
+ /**
342
+ * Slowest durable receipt-value persistence in this completion request.
343
+ * This sits inside `dispatchCallbackMs`; null means the gateway predates the
344
+ * measurement rather than that a persistence took no time.
345
+ */
346
+ receiptValuePersistMs: number | null;
347
+ dispatchCallbackMs: number | null;
348
+ authSession: 'hit' | 'miss' | 'coalesced' | 'unknown' | null;
349
+ };
350
+
351
+ export type RuntimeApiResponseMeta = {
352
+ action: RuntimeApiRequest['action'];
353
+ requestId: string | null;
354
+ /** Client-measured wall of the responding attempt: request send to response. */
355
+ roundTripMs?: number | null;
356
+ /** Absent when the gateway sent no `Server-Timing` header. */
357
+ serverTiming?: RuntimeGatewayServerTiming | null;
358
+ /**
359
+ * Serialized JSON bytes of the request body, before compression. Set on
360
+ * every request, so `wire / raw` across a run is the compression ratio the
361
+ * transport actually achieved — including the uncompressed requests, which
362
+ * is the honest denominator.
363
+ */
364
+ requestBodyRawBytes?: number | null;
365
+ /** Bytes actually put on the wire. Equals `requestBodyRawBytes` uncompressed. */
366
+ requestBodyWireBytes?: number | null;
367
+ /**
368
+ * Runner-local wall spent serializing the JSON body once for this retry
369
+ * ladder. It is deliberately outside `roundTripMs`: this synchronous work
370
+ * occupies the sandbox CPU before any request can leave the process.
371
+ */
372
+ requestBodySerializeMs?: number | null;
373
+ /**
374
+ * Runner-local wall spent packing the successful request attempt for the
375
+ * wire (gzip when the gateway has advertised support). This is also outside
376
+ * `roundTripMs`, so a CPU-bound encoder cannot masquerade as gateway or
377
+ * network time.
378
+ */
379
+ requestBodyPackMs?: number | null;
380
+ };
381
+
382
+ const RUNTIME_GATEWAY_AUTH_SESSION_VALUES = new Set([
383
+ 'hit',
384
+ 'miss',
385
+ 'coalesced',
386
+ ]);
387
+
388
+ /**
389
+ * Parse a receipt gateway `Server-Timing` header into its known entries.
390
+ *
391
+ * Deliberately total: an absent, empty, or malformed header yields nulls
392
+ * rather than an error. This is diagnostic instrumentation on the durable
393
+ * receipt path, and a gateway that changes its header must never be able to
394
+ * fail a run.
395
+ */
396
+ export function parseRuntimeGatewayServerTiming(
397
+ header: string | null | undefined,
398
+ ): RuntimeGatewayServerTiming | null {
399
+ if (typeof header !== 'string' || !header.trim()) return null;
400
+ const durations = new Map<string, number>();
401
+ let authSession: RuntimeGatewayServerTiming['authSession'] = null;
402
+ for (const rawEntry of header.split(',')) {
403
+ const parts = rawEntry.split(';');
404
+ const name = parts[0]?.trim().toLowerCase();
405
+ if (!name) continue;
406
+ for (const parameter of parts.slice(1)) {
407
+ const separator = parameter.indexOf('=');
408
+ if (separator < 0) continue;
409
+ const key = parameter.slice(0, separator).trim().toLowerCase();
410
+ const value = parameter
411
+ .slice(separator + 1)
412
+ .trim()
413
+ .replace(/^"|"$/g, '');
414
+ if (key === 'dur') {
415
+ // `Number('')` is 0, which would turn a truncated header into a
416
+ // confident zero — the one reading this instrument must never produce.
417
+ const parsed = value === '' ? Number.NaN : Number(value);
418
+ if (Number.isFinite(parsed)) durations.set(name, parsed);
419
+ } else if (key === 'desc' && name === 'auth_session' && value !== '') {
420
+ authSession = RUNTIME_GATEWAY_AUTH_SESSION_VALUES.has(value)
421
+ ? (value as 'hit' | 'miss' | 'coalesced')
422
+ : 'unknown';
423
+ }
424
+ }
425
+ }
426
+ const duration = (name: string): number | null =>
427
+ durations.has(name) ? durations.get(name)! : null;
428
+ const timing: RuntimeGatewayServerTiming = {
429
+ authLaunchMs: duration('auth_launch'),
430
+ authVerifyMs: duration('auth_verify'),
431
+ bodyParseMs: duration('body_parse'),
432
+ bodyInflateMs: duration('body_inflate'),
433
+ dispatchPoolMs: duration('dispatch_pool'),
434
+ receiptValuePersistMs: duration('receipt_value_persist'),
435
+ dispatchCallbackMs: duration('dispatch_callback'),
436
+ authSession,
437
+ };
438
+ const carriesSomething =
439
+ authSession !== null ||
440
+ timing.authLaunchMs !== null ||
441
+ timing.authVerifyMs !== null ||
442
+ timing.bodyParseMs !== null ||
443
+ timing.bodyInflateMs !== null ||
444
+ timing.dispatchPoolMs !== null ||
445
+ timing.receiptValuePersistMs !== null ||
446
+ timing.dispatchCallbackMs !== null;
447
+ return carriesSomething ? timing : null;
448
+ }
449
+
308
450
  const APP_RUNTIME_API_RETRY_DELAYS_MS = [100, 250, 500, 1_000] as const;
309
451
  const APP_RUNTIME_API_RECEIPT_RETRY_DELAYS_MS = [
310
452
  250, 500, 1_000, 2_000, 4_000, 8_000, 12_000,
@@ -1269,8 +1411,39 @@ async function postAppRuntimeApi<TResponse>(
1269
1411
  context.requestTimeoutMs ?? APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS,
1270
1412
  ),
1271
1413
  );
1414
+ // Serialize ONCE for the whole retry ladder: the body is identical across
1415
+ // attempts (that identity is what makes a receipt retry safe), so a second
1416
+ // `JSON.stringify` of a megabyte payload would be pure cost.
1417
+ const serializationStartedAt = Date.now();
1418
+ const serializedBody = JSON.stringify(body);
1419
+ const requestBodySerializeMs = Math.max(
1420
+ 0,
1421
+ Date.now() - serializationStartedAt,
1422
+ );
1272
1423
 
1273
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1424
+ let forceUncompressedNextAttempt = false;
1425
+ let usedLegacyDecoderFallback = false;
1426
+ // A legacy decoder rejection is known to be pre-dispatch, so it earns one
1427
+ // immediate plain-JSON resend even when the caller disabled normal retries.
1428
+ // It does not consume an application retry attempt or sleep.
1429
+ let attempt = 1;
1430
+ while (attempt <= maxAttempts) {
1431
+ // Re-evaluated per attempt rather than hoisted: the capability can be
1432
+ // learned from THIS request's own first attempt (or from a sibling request
1433
+ // that landed in between), so a retry compresses as soon as the origin has
1434
+ // advertised, without waiting for the next call.
1435
+ const packingStartedAt = Date.now();
1436
+ const wireBody = await gzipRuntimeApiRequestBody({
1437
+ serialized: serializedBody,
1438
+ originAcceptsGzip:
1439
+ !forceUncompressedNextAttempt && runtimeApiOriginAcceptsGzip(baseUrl),
1440
+ });
1441
+ forceUncompressedNextAttempt = false;
1442
+ const requestBodyPackMs = Math.max(0, Date.now() - packingStartedAt);
1443
+ // Started AFTER the body is packed: `roundTripMs` is the request's wall
1444
+ // from send to parsed response, and it is read against the gateway's own
1445
+ // `Server-Timing` to isolate the network. Folding local compression into it
1446
+ // would show up as network cost that no round trip explains.
1274
1447
  const attemptStartedAt = Date.now();
1275
1448
  const transportAttemptId = randomUUID();
1276
1449
  const requestSignal = context.signal
@@ -1285,7 +1458,12 @@ async function postAppRuntimeApi<TResponse>(
1285
1458
  response = await runtimeFetch(resolveAppRuntimeApiUrl(context), {
1286
1459
  method: 'POST',
1287
1460
  headers: {
1461
+ // The payload is JSON either way; `content-encoding` describes only
1462
+ // how those JSON bytes were packed for the wire.
1288
1463
  'content-type': 'application/json',
1464
+ ...(wireBody.contentEncoding
1465
+ ? { 'content-encoding': wireBody.contentEncoding }
1466
+ : {}),
1289
1467
  authorization: `Bearer ${token}`,
1290
1468
  [LEGACY_RUNTIME_RECEIPT_WIRE_HEADER]: String(
1291
1469
  LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT,
@@ -1304,7 +1482,14 @@ async function postAppRuntimeApi<TResponse>(
1304
1482
  }
1305
1483
  : {}),
1306
1484
  },
1307
- body: JSON.stringify(body),
1485
+ // DOM fetch accepts ArrayBuffer, while Node 24 deliberately rejects a
1486
+ // Uint8Array backed by an arbitrary ArrayBufferLike. Copy only the
1487
+ // compressed branch into an exact ArrayBuffer; its wire bytes remain
1488
+ // identical and the plain JSON fast path stays allocation-free.
1489
+ body:
1490
+ typeof wireBody.body === 'string'
1491
+ ? wireBody.body
1492
+ : Uint8Array.from(wireBody.body).buffer,
1308
1493
  signal: requestSignal,
1309
1494
  });
1310
1495
  const preferredMachineId =
@@ -1338,6 +1523,7 @@ async function postAppRuntimeApi<TResponse>(
1338
1523
  retryDelayMs,
1339
1524
  });
1340
1525
  await sleep(retryDelayMs);
1526
+ attempt += 1;
1341
1527
  continue;
1342
1528
  }
1343
1529
  recordAppRuntimeRetryFailure({
@@ -1364,6 +1550,21 @@ async function postAppRuntimeApi<TResponse>(
1364
1550
  boundaryLabel,
1365
1551
  });
1366
1552
  }
1553
+ // Learn the capability from EVERY response, including 4xx/5xx: an error
1554
+ // response still proves the gateway that produced it can decode a
1555
+ // compressed body, and a run whose first request fails must not be stuck
1556
+ // sending uncompressed bodies for the rest of its life. Deliberately
1557
+ // outside the fetch try/catch and defensive: this is rollout
1558
+ // instrumentation, and a header read must never be able to fail — or be
1559
+ // misreported as — a durable request.
1560
+ try {
1561
+ noteRuntimeApiCompressionCapability({
1562
+ baseUrl,
1563
+ header: response.headers?.get(RUNTIME_API_ACCEPTS_HEADER),
1564
+ });
1565
+ } catch {
1566
+ // A response without readable headers teaches nothing; it never fails.
1567
+ }
1367
1568
  if (response.ok) {
1368
1569
  try {
1369
1570
  const parsed = (await response.json()) as TResponse;
@@ -1383,6 +1584,28 @@ async function postAppRuntimeApi<TResponse>(
1383
1584
  maxAttempts,
1384
1585
  finalAttemptStartedAt: attemptStartedAt,
1385
1586
  });
1587
+ if (context.onResponseMeta) {
1588
+ try {
1589
+ context.onResponseMeta({
1590
+ action: body.action,
1591
+ requestId:
1592
+ response.headers.get('x-deepline-request-id')?.trim() || null,
1593
+ // Request send to parsed response, on the runner's clock. The
1594
+ // gateway's own timings are subtracted from this to leave the
1595
+ // network and TLS cost of the hop.
1596
+ roundTripMs: Date.now() - attemptStartedAt,
1597
+ serverTiming: parseRuntimeGatewayServerTiming(
1598
+ response.headers.get('server-timing'),
1599
+ ),
1600
+ requestBodyRawBytes: wireBody.rawBytes,
1601
+ requestBodyWireBytes: wireBody.wireBytes,
1602
+ requestBodySerializeMs,
1603
+ requestBodyPackMs,
1604
+ });
1605
+ } catch {
1606
+ // Observation must never affect durable delivery.
1607
+ }
1608
+ }
1386
1609
  return parsed;
1387
1610
  } catch (error) {
1388
1611
  await retryAppRuntimeBodyTimeoutOrThrow({
@@ -1396,6 +1619,7 @@ async function postAppRuntimeApi<TResponse>(
1396
1619
  httpStatus: response.status,
1397
1620
  boundaryLabel,
1398
1621
  });
1622
+ attempt += 1;
1399
1623
  continue;
1400
1624
  }
1401
1625
  }
@@ -1441,6 +1665,23 @@ async function postAppRuntimeApi<TResponse>(
1441
1665
  retryAfterMs: appRuntimeRetryAfterMs(response, ''),
1442
1666
  boundaryLabel,
1443
1667
  });
1668
+ attempt += 1;
1669
+ continue;
1670
+ }
1671
+ if (
1672
+ !usedLegacyDecoderFallback &&
1673
+ isLegacyRuntimeApiGzipDecoderFailure({
1674
+ contentEncoding: wireBody.contentEncoding,
1675
+ status: response.status,
1676
+ responseText,
1677
+ })
1678
+ ) {
1679
+ // An older gateway reached through a rolling hostname rejected gzip
1680
+ // before JSON parsing, so no runtime action was dispatched. Demote the
1681
+ // hostname capability and resend the same serialized request plain.
1682
+ revokeRuntimeApiCompressionCapability(baseUrl);
1683
+ forceUncompressedNextAttempt = true;
1684
+ usedLegacyDecoderFallback = true;
1444
1685
  continue;
1445
1686
  }
1446
1687
  const responseCode = appRuntimeErrorCode(response, responseText);
@@ -1481,6 +1722,7 @@ async function postAppRuntimeApi<TResponse>(
1481
1722
  httpStatus: response.status,
1482
1723
  });
1483
1724
  await sleep(retryDelayMs);
1725
+ attempt += 1;
1484
1726
  continue;
1485
1727
  }
1486
1728
  recordAppRuntimeRetryFailure({
@@ -2393,6 +2635,27 @@ export async function completeRuntimeStepReceiptsViaAppRuntime(
2393
2635
  });
2394
2636
  }
2395
2637
 
2638
+ /**
2639
+ * Publish receipt-owned R2 references written by the trusted execution relay.
2640
+ * This deliberately has its own action name so an older gateway rejects it
2641
+ * instead of treating a missing legacy `output` as a successful null write.
2642
+ */
2643
+ export async function completeRuntimeStepReceiptsByReferenceViaAppRuntime(
2644
+ context: WorkerRuntimeApiContext,
2645
+ input: Omit<
2646
+ Extract<
2647
+ RuntimeApiRequest,
2648
+ { action: 'complete_runtime_step_receipts_by_reference' }
2649
+ >,
2650
+ 'action'
2651
+ >,
2652
+ ): Promise<Array<RuntimeStepReceipt | null>> {
2653
+ return await postAppRuntimeApi<Array<RuntimeStepReceipt | null>>(context, {
2654
+ action: 'complete_runtime_step_receipts_by_reference',
2655
+ ...input,
2656
+ });
2657
+ }
2658
+
2396
2659
  export async function failRuntimeStepReceiptViaAppRuntime(
2397
2660
  context: WorkerRuntimeApiContext,
2398
2661
  input: Omit<