deepline 0.3.127 → 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 +254 -38
  113. package/dist/cli/index.mjs +254 -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
@@ -10,7 +10,7 @@ import type {
10
10
  } from '../protocol';
11
11
 
12
12
  export type PlayRunnerRuntimeResource = {
13
- kind: 'daytona_sandbox' | 'modal_sandbox';
13
+ kind: 'daytona_sandbox' | 'modal_sandbox' | 'agentcore_runtime_session';
14
14
  sandboxId: string;
15
15
  /** Runtime estate that owns provider credentials and durable cleanup. */
16
16
  runtimeEnvironment?: 'preview' | 'production';
@@ -19,6 +19,12 @@ export type PlayRunnerRuntimeResource = {
19
19
  daytonaOrganizationId?: string;
20
20
  /** Stable, non-secret Modal ownership domain returned at acquisition. */
21
21
  modalAppId?: string;
22
+ /**
23
+ * Stable, non-secret ownership domain for a session-based runtime. The
24
+ * provider adapter supplies this value; the shared flow only persists it as
25
+ * the cleanup routing domain.
26
+ */
27
+ agentcoreRuntimeArn?: string;
22
28
  billingStartedAt: number;
23
29
  billingEndedAt?: number | null;
24
30
  /**
@@ -85,7 +91,7 @@ export type PlayRunnerRuntimeLifecycleEvent = {
85
91
  * resource identity to cross the no-replay boundary.
86
92
  */
87
93
  export type PlayRunnerCommandAcceptance = {
88
- provider: 'daytona' | 'modal';
94
+ provider: 'daytona' | 'modal' | 'agentcore_runtime';
89
95
  sandboxId: string;
90
96
  };
91
97
 
@@ -171,7 +177,7 @@ export interface PlayRunnerCallbacks {
171
177
  onAmbiguousModalCreateResolved?: () => void | Promise<void>;
172
178
  /** Reserve one physical-sandbox slot immediately before a provider create. */
173
179
  reserveSandboxCapacity?: (
174
- provider: 'daytona' | 'modal',
180
+ provider: 'daytona' | 'modal' | 'agentcore_runtime',
175
181
  providerAttempt?: number,
176
182
  ) => Promise<{ leaseId: string; providerOperationToken: string | null }>;
177
183
  /** Release an unbound reservation when provider create never produced a resource. */
@@ -182,11 +188,11 @@ export interface PlayRunnerCallbacks {
182
188
  * pre-code recovery outcome instead.
183
189
  */
184
190
  shouldAttemptSandboxProvider?: (
185
- provider: 'daytona' | 'modal',
191
+ provider: 'daytona' | 'modal' | 'agentcore_runtime',
186
192
  ) => Promise<{ allowed: boolean; probeToken: string | null }>;
187
193
  /** A durably recorded resource may close only the probe that owns it. */
188
194
  markSandboxProviderHealthy?: (
189
- provider: 'daytona' | 'modal',
195
+ provider: 'daytona' | 'modal' | 'agentcore_runtime',
190
196
  probeToken: string | null,
191
197
  ) => Promise<void>;
192
198
  /**
@@ -6,6 +6,10 @@ import type { PlayRunLedgerEvent } from './run-ledger';
6
6
  import type { WorkReceiptFailureKind } from './work-receipts';
7
7
  import type { ToolExecutionFailureV1 } from '../plays/tool-execution-error';
8
8
  import type { DocflowObservationUpsert } from './docflow-observation';
9
+ import type {
10
+ RuntimeReceiptValueInlineFields,
11
+ RuntimeReceiptValueReference,
12
+ } from './runtime-receipt-value';
9
13
 
10
14
  export type RuntimeAppendRunEventsAction = {
11
15
  action: 'append_run_events';
@@ -96,12 +100,16 @@ export type RuntimeReceiptAction =
96
100
  playName: string;
97
101
  runId: string;
98
102
  key: string;
103
+ /** Omit receipt outputs while probing completed-cache manifests. */
104
+ hydrateOutput?: boolean;
99
105
  }
100
106
  | {
101
107
  action: 'get_runtime_step_receipts';
102
108
  playName: string;
103
109
  runId: string;
104
110
  keys: string[];
111
+ /** Omit receipt outputs while probing completed-cache manifests. */
112
+ hydrateOutput?: boolean;
105
113
  }
106
114
  | {
107
115
  action: 'acquire_runtime_receipt_execution_lock';
@@ -123,6 +131,8 @@ export type RuntimeReceiptAction =
123
131
  playName: string;
124
132
  runId: string;
125
133
  key: string;
134
+ /** Omit terminal output bodies while establishing a receipt claim. */
135
+ hydrateOutput?: boolean;
126
136
  leaseId?: string;
127
137
  runAttempt?: number | null;
128
138
  leaseAware?: boolean;
@@ -135,6 +145,8 @@ export type RuntimeReceiptAction =
135
145
  playName: string;
136
146
  runId: string;
137
147
  keys: string[];
148
+ /** Omit terminal output bodies while establishing receipt claims. */
149
+ hydrateOutput?: boolean;
138
150
  leaseIds?: string[];
139
151
  runAttempt?: number | null;
140
152
  leaseAware?: boolean;
@@ -150,6 +162,7 @@ export type RuntimeReceiptAction =
150
162
  runAttempt?: number | null;
151
163
  leaseId?: string | null;
152
164
  output: unknown;
165
+ inlineFields?: RuntimeReceiptValueInlineFields;
153
166
  }
154
167
  | {
155
168
  action: 'release_runtime_step_receipt';
@@ -205,6 +218,18 @@ export type RuntimeReceiptAction =
205
218
  runId: string;
206
219
  receipts: RuntimeReceiptCompletion[];
207
220
  }
221
+ | {
222
+ /**
223
+ * New named action: the execution relay has already persisted the full
224
+ * serialized result to the receipt-owned R2 key. A receiver that does
225
+ * not know this action rejects it loudly; the runner then retries the
226
+ * established full-payload completion without replaying provider work.
227
+ */
228
+ action: 'complete_runtime_step_receipts_by_reference';
229
+ playName: string;
230
+ runId: string;
231
+ receipts: RuntimeReceiptReferenceCompletion[];
232
+ }
208
233
  | {
209
234
  action: 'fail_runtime_step_receipts';
210
235
  playName: string;
@@ -234,6 +259,11 @@ export type RuntimeReceiptLease = {
234
259
 
235
260
  export type RuntimeReceiptCompletion = RuntimeReceiptLease & {
236
261
  output: unknown;
262
+ inlineFields?: RuntimeReceiptValueInlineFields;
263
+ };
264
+
265
+ export type RuntimeReceiptReferenceCompletion = RuntimeReceiptLease & {
266
+ outputRef: RuntimeReceiptValueReference;
237
267
  };
238
268
 
239
269
  export type RuntimeReceiptFailure = RuntimeReceiptLease & {
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Wire compression for runtime API request bodies.
3
+ *
4
+ * WHY THIS EXISTS. A fixture sandbox sits ~95 ms from the Fly receipt gateway
5
+ * (Daytona eu, Fly iad). Measured on run 34104186304, a ~100 KB receipt
6
+ * completion request (12 receipts x ~8 KB) took 765 ms p50 on the runner's
7
+ * clock while the gateway's own `Server-Timing` accounted for 50 ms of it. The
8
+ * missing 700 ms is not the gateway and not the network's bandwidth: it is TCP
9
+ * congestion-window growth. A keep-alive connection that idles between
10
+ * completion batches has its cwnd reset toward the initial window (~10 MSS,
11
+ * ~14 KB), so a 100 KB upload is delivered over 5-8 round trips of ~95 ms each
12
+ * before the gateway has the whole body to parse. jai's ~1 MB completion
13
+ * requests pay the same arithmetic at ~5.4 s.
14
+ *
15
+ * Moving the sandbox region is an ops action and is being pursued separately.
16
+ * Independent of it, the round trips are a function of BYTES, so shrinking the
17
+ * body shrinks the wall directly. Measured gzip level 6 on realistic receipt
18
+ * payloads: high-entropy enrichment output compresses to ~24% (a 100 KB body
19
+ * becomes ~24 KB, which fits in 1-2 congestion windows instead of 5-8), and
20
+ * repetitive output to ~4-6%.
21
+ *
22
+ * COMPATIBILITY. A gateway that does not understand `content-encoding: gzip`
23
+ * would hand gzip bytes to `JSON.parse` and fail every request, so the client
24
+ * never compresses speculatively. It compresses only after it has seen the
25
+ * gateway advertise `x-deepline-runtime-api-accepts: gzip` on a response from
26
+ * that base URL. The first request to any origin is therefore always
27
+ * uncompressed, and an older gateway (or an app route that has not shipped the
28
+ * decoder) simply never advertises and never receives a compressed body. In the
29
+ * other direction, a newer gateway keeps accepting uncompressed bodies from
30
+ * older runners forever: the encoding is opt-in per request, not a version gate.
31
+ */
32
+
33
+ import { gzip, gunzip } from 'node:zlib';
34
+ import { promisify } from 'node:util';
35
+
36
+ const gzipAsync = promisify(gzip);
37
+ const gunzipAsync = promisify(gunzip);
38
+
39
+ /**
40
+ * Bodies at or above this size are gzipped before they are sent.
41
+ *
42
+ * 16 KiB is one congestion window plus change. Below it the body already fits
43
+ * in the first flight of an established connection (initial window ~10 MSS,
44
+ * ~14 KB), so compressing buys no round trip — it only spends CPU on the
45
+ * runner's single vCPU and adds a gzip header the gateway has to inflate. At
46
+ * and above it, every ~14 KB removed from the wire removes roughly one ~95 ms
47
+ * round trip from the request (measured above: 100 KB = 765 ms client / 50 ms
48
+ * server; the ~700 ms difference is cwnd growth, not bandwidth).
49
+ *
50
+ * The threshold is compared against the UTF-8 byte length of the serialized
51
+ * body, not its character count.
52
+ */
53
+ export const RUNTIME_API_REQUEST_COMPRESSION_MIN_BYTES = 16 * 1024;
54
+
55
+ /**
56
+ * zlib level for request bodies. Level 6 is the measured optimum at one vCPU
57
+ * on the sandbox's ~15 Mbit/s link (51 runs, 6.17 MB raw batch): level 6
58
+ * costs 539 ms and ships 889 KB, level 1 costs 418 ms and ships 1306 KB, no
59
+ * gzip costs 183 ms and ships 6025 KB. Batches per second are bounded by
60
+ * whichever side binds: min(CPU, link) = 1.90 at level 6, 1.44 at level 1,
61
+ * 0.31 with no gzip. Level 1 saves 121 ms of CPU and buys 417 KB of wire that
62
+ * costs 223 ms. If the search moves, it moves up from 6, not down. See the
63
+ * cost table in `gzipRuntimeApiRequestBody`.
64
+ */
65
+ export const RUNTIME_API_REQUEST_COMPRESSION_LEVEL = 6;
66
+
67
+ /**
68
+ * The gateway's advertisement that it can decode compressed request bodies.
69
+ *
70
+ * Present on EVERY gateway response, including errors and health probes, so a
71
+ * client learns the capability from whatever response it happens to get first
72
+ * rather than needing a successful request of a particular action.
73
+ */
74
+ export const RUNTIME_API_ACCEPTS_HEADER = 'x-deepline-runtime-api-accepts';
75
+
76
+ /** The only encoding this contract defines. */
77
+ export const RUNTIME_API_ACCEPTS_GZIP = 'gzip';
78
+
79
+ /** Does an `x-deepline-runtime-api-accepts` value advertise gzip? */
80
+ export function runtimeApiAcceptsHeaderAdvertisesGzip(
81
+ header: string | null | undefined,
82
+ ): boolean {
83
+ if (typeof header !== 'string') return false;
84
+ return header
85
+ .split(',')
86
+ .some((token) => token.trim().toLowerCase() === RUNTIME_API_ACCEPTS_GZIP);
87
+ }
88
+
89
+ /** Is a request's `content-encoding` gzip? */
90
+ export function isGzipContentEncoding(
91
+ header: string | null | undefined,
92
+ ): boolean {
93
+ return typeof header === 'string' && header.trim().toLowerCase() === 'gzip';
94
+ }
95
+
96
+ /**
97
+ * Origins observed to advertise gzip, keyed by base URL origin.
98
+ *
99
+ * State is per process and per origin, never per request: a runner talks to one
100
+ * receipt gateway and one app runtime API for its whole life, so one observed
101
+ * response enables compression until that origin proves otherwise. The state
102
+ * cannot be monotonic: a rolling Fly fleet can route two requests for the same
103
+ * hostname to different Machine revisions, and an older revision cannot decode
104
+ * a gzip body even if its newer peer advertised support moments earlier.
105
+ */
106
+ const gzipCapableOrigins = new Set<string>();
107
+
108
+ function compressionOriginKey(baseUrl: string): string {
109
+ const trimmed = baseUrl.trim();
110
+ if (!trimmed) return '';
111
+ try {
112
+ return new URL(trimmed).origin;
113
+ } catch {
114
+ // A non-URL base is still a stable key; it just cannot be normalized.
115
+ return trimmed.replace(/\/$/, '');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Record what one response said about its gateway's request-decoding support.
121
+ *
122
+ * Called for every response, successful or not. A response that carries no
123
+ * advertisement teaches nothing and never revokes a prior advertisement.
124
+ */
125
+ export function noteRuntimeApiCompressionCapability(input: {
126
+ baseUrl: string;
127
+ header: string | null | undefined;
128
+ }): void {
129
+ if (!runtimeApiAcceptsHeaderAdvertisesGzip(input.header)) return;
130
+ const key = compressionOriginKey(input.baseUrl);
131
+ if (key) gzipCapableOrigins.add(key);
132
+ }
133
+
134
+ /**
135
+ * Forget an origin after its legacy JSON decoder rejected a gzip body.
136
+ *
137
+ * The caller retries that exact request as plain JSON before any durable action
138
+ * can have run. A subsequent response from a newer Machine may advertise gzip
139
+ * again, but a rolling hostname is never treated as permanently capable.
140
+ */
141
+ export function revokeRuntimeApiCompressionCapability(baseUrl: string): void {
142
+ const key = compressionOriginKey(baseUrl);
143
+ if (key) gzipCapableOrigins.delete(key);
144
+ }
145
+
146
+ /**
147
+ * Is this the exact response emitted when a pre-gzip gateway parsed compressed
148
+ * bytes as JSON? Restrict the fallback to this known pre-dispatch failure so a
149
+ * normal 400 from a gzip-capable gateway is never replayed.
150
+ */
151
+ export function isLegacyRuntimeApiGzipDecoderFailure(input: {
152
+ contentEncoding: RuntimeApiRequestBody['contentEncoding'];
153
+ status: number;
154
+ responseText: string;
155
+ }): boolean {
156
+ if (input.contentEncoding !== 'gzip' || input.status !== 400) return false;
157
+ try {
158
+ const parsed = JSON.parse(input.responseText) as { error?: unknown };
159
+ return parsed.error === 'JSON body is required.';
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /** Has this origin advertised that it can decode gzipped request bodies? */
166
+ export function runtimeApiOriginAcceptsGzip(baseUrl: string): boolean {
167
+ const key = compressionOriginKey(baseUrl);
168
+ return key ? gzipCapableOrigins.has(key) : false;
169
+ }
170
+
171
+ /** Forget every observed capability. Tests only; there is no production reset. */
172
+ export function resetRuntimeApiCompressionCapabilities(): void {
173
+ gzipCapableOrigins.clear();
174
+ }
175
+
176
+ export type RuntimeApiRequestBody = {
177
+ /** What goes on the wire. */
178
+ body: string | Uint8Array;
179
+ /** `gzip` when compressed, otherwise null (send no `content-encoding`). */
180
+ contentEncoding: 'gzip' | null;
181
+ /** UTF-8 bytes of the serialized JSON, before any compression. */
182
+ rawBytes: number;
183
+ /** Bytes actually sent. Equals `rawBytes` when uncompressed. */
184
+ wireBytes: number;
185
+ };
186
+
187
+ /**
188
+ * Compress a serialized request body when it is worth compressing.
189
+ *
190
+ * ASYNC, NOT SYNC. Measured on this codebase's realistic receipt-completion
191
+ * payloads (level 6): a 100 KB body costs 2.2 ms and a 1 MB body 26.5 ms, and
192
+ * `gzipSync` and `promisify(gzip)` have the SAME wall (2.24/2.37 ms and
193
+ * 26.55/27.13 ms). Identical wall, but the async form runs on the libuv
194
+ * threadpool instead of blocking the runner's single-vCPU event loop, so the
195
+ * map keeps advancing rows during the compression of a large completion batch.
196
+ * A 26 ms synchronous stall on every 1 MB completion is exactly the kind of
197
+ * per-batch pause the throughput work is removing, so the free version wins.
198
+ */
199
+ export async function gzipRuntimeApiRequestBody(input: {
200
+ serialized: string;
201
+ /** Compress only when the destination has advertised support. */
202
+ originAcceptsGzip: boolean;
203
+ minBytes?: number;
204
+ }): Promise<RuntimeApiRequestBody> {
205
+ const rawBytes = Buffer.byteLength(input.serialized, 'utf8');
206
+ const minBytes = input.minBytes ?? RUNTIME_API_REQUEST_COMPRESSION_MIN_BYTES;
207
+ if (!input.originAcceptsGzip || rawBytes < minBytes) {
208
+ return {
209
+ body: input.serialized,
210
+ contentEncoding: null,
211
+ rawBytes,
212
+ wireBytes: rawBytes,
213
+ };
214
+ }
215
+ const compressed = await gzipAsync(Buffer.from(input.serialized, 'utf8'), {
216
+ level: RUNTIME_API_REQUEST_COMPRESSION_LEVEL,
217
+ });
218
+ return {
219
+ body: compressed,
220
+ contentEncoding: 'gzip',
221
+ rawBytes,
222
+ wireBytes: compressed.byteLength,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * A gzipped request body whose DECOMPRESSED size exceeds the gateway limit.
228
+ *
229
+ * Loud and typed rather than a generic parse failure: a compressed body is the
230
+ * one shape where the transport's own byte guard cannot see the real size, so
231
+ * the caller must be told the limit it actually crossed. The gateway surfaces
232
+ * this as the same 413 an oversized uncompressed body gets.
233
+ */
234
+ export class RuntimeApiDecompressedBodyTooLargeError extends Error {
235
+ constructor(
236
+ readonly maxBytes: number,
237
+ readonly wireBytes: number,
238
+ ) {
239
+ super(
240
+ `Runtime API request body decompressed to more than the ${maxBytes} byte limit ` +
241
+ `(gzip wire size ${wireBytes} bytes). Send fewer receipts per request.`,
242
+ );
243
+ this.name = 'RuntimeApiDecompressedBodyTooLargeError';
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Inflate a gzipped request body under a hard output cap.
249
+ *
250
+ * `maxOutputLength` makes zlib abort the inflate as soon as the output crosses
251
+ * the cap, so a zip bomb costs the gateway the bytes it was actually sent plus
252
+ * one buffer of the limit — never the decompressed size. Without it a 500 KB
253
+ * body could expand to gigabytes before any byte guard downstream saw it.
254
+ *
255
+ * Async for the same reason the client compresses asynchronously: inflate is
256
+ * cheap (0.2 ms for 100 KB, 2.2 ms for 1 MB) but the gateway multiplexes many
257
+ * runners on one event loop, and the threadpool version costs ~1 ms more wall
258
+ * for work that no longer blocks every other in-flight request.
259
+ */
260
+ export async function gunzipRuntimeApiRequestBody(input: {
261
+ body: Buffer;
262
+ maxBytes: number;
263
+ }): Promise<Buffer> {
264
+ try {
265
+ return await gunzipAsync(input.body, { maxOutputLength: input.maxBytes });
266
+ } catch (error) {
267
+ const code = (error as { code?: unknown } | null)?.code;
268
+ if (code === 'ERR_BUFFER_TOO_LARGE' || code === 'ERR_OUT_OF_RANGE') {
269
+ throw new RuntimeApiDecompressedBodyTooLargeError(
270
+ input.maxBytes,
271
+ input.body.byteLength,
272
+ );
273
+ }
274
+ throw error;
275
+ }
276
+ }