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
@@ -6,6 +6,15 @@ import {
6
6
  } from './tool-result';
7
7
  import { assertNoSecretTaint } from './secret-capability';
8
8
  import type { RuntimeStepReceipt } from './ctx-types';
9
+ import {
10
+ percentile,
11
+ runtimeJsonByteLength,
12
+ type RuntimeMemoryLedgerHandle,
13
+ } from './memory-ledger';
14
+ import type {
15
+ RuntimeMemoryOccupancy,
16
+ RuntimeMemoryOccupancyReader,
17
+ } from './map-memory-limits';
9
18
  import type { DurableReceiptRecoverySource } from './tool-execution-outcome';
10
19
  import {
11
20
  isPlayExecutionSuspendedError,
@@ -30,6 +39,619 @@ import type {
30
39
 
31
40
  export const DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
32
41
  export const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
42
+ // A running receipt owned elsewhere is re-read with exponential backoff up to
43
+ // this cap. Fixed 250 ms polls from every waiting group were most of the
44
+ // gateway read traffic in a 1k map. Because the delay grows, the attempt count
45
+ // alone no longer bounds wall time: every backoff loop must clamp itself to
46
+ // `runtimeReceiptWaitBudgetMs`.
47
+ export const DURABLE_RECEIPT_WAIT_MAX_DELAY_MS = 2_000;
48
+ // The hydration lane re-examines its queue on this cadence while turns are
49
+ // waiting, so headroom freed outside the lane reaches a waiter promptly. It is
50
+ // a probe interval, not a width: nothing about it bounds concurrency.
51
+ const RUNTIME_RECEIPT_HYDRATION_PROBE_MS = 25;
52
+
53
+ /** Backoff delay before re-reading a running receipt on the given attempt (1-based). */
54
+ export function runtimeReceiptWaitDelayMs(attempt: number): number {
55
+ const exponent = Math.max(0, Math.min(attempt, 8) - 1);
56
+ return Math.min(
57
+ DURABLE_RECEIPT_WAIT_MAX_DELAY_MS,
58
+ DURABLE_RECEIPT_WAIT_DELAY_MS * 2 ** exponent,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Whether a claim that came back `existing` is actually blocked by live work.
64
+ *
65
+ * A non-forced claim's status set is `('pending','failed')`, so a `running`
66
+ * row returns `existing` whether its owner is alive or long dead. Only an
67
+ * unexpired lease proves the former; anything else must be reclaimed and
68
+ * re-executed rather than waited on.
69
+ */
70
+ export function runtimeReceiptClaimLeaseIsLive(
71
+ claim: Pick<RuntimeStepReceipt, 'leaseExpiresAt'>,
72
+ nowMs: number = Date.now(),
73
+ ): boolean {
74
+ const expiresAtMs = Date.parse(claim.leaseExpiresAt ?? '');
75
+ return !Number.isFinite(expiresAtMs) || expiresAtMs > nowMs;
76
+ }
77
+
78
+ /** Wall-clock budget that a fixed-delay attempt count used to express. */
79
+ export function runtimeReceiptWaitBudgetMs(
80
+ maxAttempts: number,
81
+ delayMs: number = DURABLE_RECEIPT_WAIT_DELAY_MS,
82
+ ): number {
83
+ return Math.max(delayMs, maxAttempts * delayMs);
84
+ }
85
+
86
+ /**
87
+ * Settled per-key hydration samples the lane keeps to price a turn. Bounded,
88
+ * so past this many the reservoir below chooses which to keep.
89
+ */
90
+ export const RUNTIME_RECEIPT_HYDRATION_SAMPLE_RESERVOIR = 256;
91
+ /**
92
+ * Settled turns the lane needs before its own measured p90 per-key cost
93
+ * replaces the ledger's per-owner price. Sixteen matches the settled-row count
94
+ * map admission waits for before it trusts a p90, and for the same reason: it
95
+ * is enough for a percentile to mean something without making the first wave
96
+ * of a map wait for a tenth of itself.
97
+ */
98
+ export const RUNTIME_RECEIPT_HYDRATION_SAMPLE_TURNS = 16;
99
+
100
+ /**
101
+ * Bounded semantic origins for completed-receipt reads. These are runtime
102
+ * paths, never receipt keys, tool ids, or caller inputs, so the telemetry that
103
+ * explains a large read cannot become another retention surface.
104
+ */
105
+ export const RUNTIME_RECEIPT_HYDRATION_SOURCES = [
106
+ 'durable_operation',
107
+ 'direct_tool_legacy_cache',
108
+ 'direct_tool_result_receipt',
109
+ 'map_cohort_read',
110
+ 'map_legacy_claim',
111
+ 'map_receipt_wait',
112
+ 'receipt_completion_read_back',
113
+ 'unknown',
114
+ ] as const;
115
+
116
+ export type RuntimeReceiptHydrationSource =
117
+ (typeof RUNTIME_RECEIPT_HYDRATION_SOURCES)[number];
118
+
119
+ export type RuntimeReceiptHydrationSourceSnapshot = {
120
+ turns: number;
121
+ receiptKeys: number;
122
+ bytesAdmitted: number;
123
+ bytesMeasured: number;
124
+ };
125
+
126
+ export type RuntimeReceiptHydrationGateSnapshot = {
127
+ /** Turns in flight, and turns queued behind the byte budget. */
128
+ active: number;
129
+ waiting: number;
130
+ /** Turns taken, and how long callers queued for them. */
131
+ turns: number;
132
+ queuedTurns: number;
133
+ totalWaitMs: number;
134
+ maxWaitMs: number;
135
+ /** Ledger bytes reserved by the turns in flight right now. */
136
+ bytesHeld: number;
137
+ bytesHeldPeak: number;
138
+ /** Ledger bytes this lane has admitted over its life. */
139
+ bytesAdmitted: number;
140
+ /** Bytes turns reported actually holding, replacing their reservation. */
141
+ bytesMeasured: number;
142
+ /** Admission attempts the byte budget refused. */
143
+ holds: number;
144
+ /** Collections taken to answer a hold before it was declared. */
145
+ holdCollections: number;
146
+ /**
147
+ * Turns admitted because nothing else was in flight. A turn larger than the
148
+ * whole budget still runs: one turn may always run, or a map whose payloads
149
+ * exceed the target would never finish a single row.
150
+ */
151
+ progressAdmissions: number;
152
+ /** Turns admitted with no readable heap, where byte admission cannot apply. */
153
+ unpricedAdmissions: number;
154
+ /** Ledger bytes the last decision priced one receipt key at. */
155
+ perKeyBytes: number | null;
156
+ /** Settled per-key samples behind that price; zero means it is the prior. */
157
+ perKeySamples: number;
158
+ /** Live bytes under the heap target at the last decision. */
159
+ headroomBytes: number | null;
160
+ /** Bounded provenance for the byte totals above. */
161
+ bySource: Record<
162
+ RuntimeReceiptHydrationSource,
163
+ RuntimeReceiptHydrationSourceSnapshot
164
+ >;
165
+ };
166
+
167
+ export type RuntimeReceiptHydrationTurn = {
168
+ /**
169
+ * Replace this turn's reservation with the bytes the read actually holds.
170
+ * Called once the response is in hand and before it is expanded, so the
171
+ * ledger stops carrying an estimate the moment a measurement exists.
172
+ */
173
+ settleBytes(bytes: number): void;
174
+ /** Drop the reservation. Idempotent. */
175
+ release(): void;
176
+ };
177
+
178
+ export type RuntimeReceiptHydrationGate = {
179
+ /**
180
+ * Take a turn on the hydration lane. `receiptKeys` is how many receipt keys
181
+ * the turn will read: the turn's expected bytes are that count times the
182
+ * price of one key. A manifest-aware reader may instead supply its exact
183
+ * serialized `expectedBytes` before redeeming the body.
184
+ */
185
+ acquire(input?: {
186
+ receiptKeys?: number;
187
+ expectedBytes?: number;
188
+ source?: RuntimeReceiptHydrationSource;
189
+ /**
190
+ * Wall-clock bound on the queue wait. A turn that cannot be admitted
191
+ * within it rejects with `RuntimeReceiptHydrationTurnTimeoutError` rather
192
+ * than parking forever behind a holder that never released.
193
+ */
194
+ timeoutMs?: number;
195
+ /** Abort the queue wait; the signal's reason is what rejects. */
196
+ signal?: AbortSignal;
197
+ }): Promise<RuntimeReceiptHydrationTurn>;
198
+ snapshot(): RuntimeReceiptHydrationGateSnapshot;
199
+ /**
200
+ * Release the admission timer and reject every still-queued turn. A closed
201
+ * lane will never admit again, so leaving those promises pending is the
202
+ * unbounded wait this contract forbids. Idempotent.
203
+ */
204
+ close(): void;
205
+ };
206
+
207
+ /**
208
+ * The completed-receipt hydration lane, admitted by bytes.
209
+ *
210
+ * A hydration turn holds the receipt gateway's response — the serialized
211
+ * outputs of every key it read — from the moment the response is parsed until
212
+ * the turn returns, plus the JSON expansion of those outputs while the turn
213
+ * hands them to their callers. Nothing else in the runtime accounts for those
214
+ * bytes, so this lane registers them on the byte ledger itself: an expected
215
+ * size on admission, the measured size once the response is read, and nothing
216
+ * at all once the turn is over.
217
+ *
218
+ * It used to be a counting semaphore of 8. That bound had no relation to
219
+ * bytes: eight turns of a megabyte-per-key shape hold two orders of magnitude
220
+ * more memory than eight turns of a fresh map's empty claims, and the constant
221
+ * could not tell them apart. Worse, the slot is taken at the dispatch group's
222
+ * claim and held across the gateway round trip (jacob p50 2.2 s, p90 6.3 s)
223
+ * before a single byte is expanded, so on a 1,000-row map 58% of turns queued
224
+ * 3.8 s each for a lane that was not protecting anything measurable.
225
+ *
226
+ * Policy, in order:
227
+ * 1. Nothing in flight admits immediately. One turn may always run, so a
228
+ * turn whose payload exceeds the whole budget still makes progress
229
+ * instead of deadlocking the run. This is the only floor in the lane and
230
+ * it is exactly one turn.
231
+ * 2. A turn's expected bytes are `receiptKeys x perKeyBytes`. `perKeyBytes`
232
+ * is the lane's own measured p90 of settled per-key sizes once it has
233
+ * enough samples, and until then the ledger's per-owner price — the same
234
+ * p90-or-prior map row admission uses.
235
+ * 3. Admission compares those expected live bytes — the reservation times
236
+ * the `hydrationReadBack` factor, the category it registers on — against
237
+ * the headroom `runtimeMemoryOccupancy` reports: heap target minus
238
+ * (baseline + the ledger's per-category prediction). Because the
239
+ * reservation is registered on the same ledger, an admitted turn's bytes
240
+ * are already inside the next turn's occupancy; the lane does not keep a
241
+ * second set of books.
242
+ * 4. Over budget is a question about garbage before it is a verdict about
243
+ * turns: the lane collects (rate limited, through the shared reader) and
244
+ * re-reads before it holds.
245
+ * 5. Waiters are FIFO. The head of the queue is the only candidate, so a
246
+ * small turn cannot overtake a large one that has been waiting.
247
+ */
248
+ export function createRuntimeReceiptHydrationGate(input: {
249
+ /** Byte occupancy of this process, shared with map row admission. */
250
+ occupancy: RuntimeMemoryOccupancyReader;
251
+ /** Registers this lane's reservations on the runtime byte ledger. */
252
+ reserve: (bytes: number) => RuntimeMemoryLedgerHandle;
253
+ /**
254
+ * Re-examines the queue while turns wait, so headroom freed by work outside
255
+ * this lane (a row settling, a body released) reaches a waiter without
256
+ * waiting for another turn to finish. Runs only while someone is waiting.
257
+ */
258
+ probeIntervalMs?: number;
259
+ sampleTurns?: number;
260
+ sampleReservoirSize?: number;
261
+ /** Uniform [0, 1) source for the sample reservoir; tests inject a sequence. */
262
+ random?: () => number;
263
+ now?: () => number;
264
+ }): RuntimeReceiptHydrationGate {
265
+ const occupancyReader = input.occupancy;
266
+ const now = input.now ?? Date.now;
267
+ const probeIntervalMs = Math.max(
268
+ 1,
269
+ Math.floor(input.probeIntervalMs ?? RUNTIME_RECEIPT_HYDRATION_PROBE_MS),
270
+ );
271
+ const sampleTurns = Math.max(
272
+ 1,
273
+ Math.floor(input.sampleTurns ?? RUNTIME_RECEIPT_HYDRATION_SAMPLE_TURNS),
274
+ );
275
+ const sampleReservoirSize = Math.max(
276
+ 1,
277
+ Math.floor(
278
+ input.sampleReservoirSize ?? RUNTIME_RECEIPT_HYDRATION_SAMPLE_RESERVOIR,
279
+ ),
280
+ );
281
+ const uniform = (): number => {
282
+ const value = input.random ? input.random() : Math.random();
283
+ return Number.isFinite(value)
284
+ ? Math.min(0.999999999, Math.max(0, value))
285
+ : 0;
286
+ };
287
+
288
+ let active = 0;
289
+ let turns = 0;
290
+ let queuedTurns = 0;
291
+ let totalWaitMs = 0;
292
+ let maxWaitMs = 0;
293
+ let bytesHeld = 0;
294
+ let bytesHeldPeak = 0;
295
+ let bytesAdmitted = 0;
296
+ let bytesMeasured = 0;
297
+ let holds = 0;
298
+ let holdCollections = 0;
299
+ let progressAdmissions = 0;
300
+ let unpricedAdmissions = 0;
301
+ let perKeyBytes: number | null = null;
302
+ let headroomBytes: number | null = null;
303
+ let closed = false;
304
+ let sampledTurns = 0;
305
+ const perKeySamples: number[] = [];
306
+ const bySource = Object.fromEntries(
307
+ RUNTIME_RECEIPT_HYDRATION_SOURCES.map((source) => [
308
+ source,
309
+ { turns: 0, receiptKeys: 0, bytesAdmitted: 0, bytesMeasured: 0 },
310
+ ]),
311
+ ) as Record<
312
+ RuntimeReceiptHydrationSource,
313
+ RuntimeReceiptHydrationSourceSnapshot
314
+ >;
315
+ type HydrationWaiter = {
316
+ receiptKeys: number;
317
+ expectedBytes: number | null;
318
+ source: RuntimeReceiptHydrationSource;
319
+ queuedAtMs: number;
320
+ /** Set when the waiter is admitted, so `acquire` can tell a queue apart
321
+ * from an immediate grant without scanning the queue for itself. */
322
+ admitted: boolean;
323
+ resolve: (turn: RuntimeReceiptHydrationTurn) => void;
324
+ /** Settles a waiter the lane will never admit. Cleared once admitted. */
325
+ reject: (reason: unknown) => void;
326
+ abandon: (() => void) | null;
327
+ };
328
+ const waiters: HydrationWaiter[] = [];
329
+ let timer: ReturnType<typeof setInterval> | null = null;
330
+
331
+ const recordPerKeySample = (bytesPerKey: number): void => {
332
+ sampledTurns += 1;
333
+ if (perKeySamples.length < sampleReservoirSize) {
334
+ perKeySamples.push(bytesPerKey);
335
+ return;
336
+ }
337
+ // Vitter's Algorithm R, for the same reason the ledger's settled-owner
338
+ // reservoir uses it: a cache-hit wave and a fresh-claim wave settle at
339
+ // very different sizes, and keeping the most recent samples would price
340
+ // the lane from whichever wave happened to be last.
341
+ const slot = Math.floor(uniform() * sampledTurns);
342
+ if (slot < sampleReservoirSize) perKeySamples[slot] = bytesPerKey;
343
+ };
344
+
345
+ /** Ledger bytes one receipt key is expected to cost. */
346
+ const priceKey = (occupancy: RuntimeMemoryOccupancy): number => {
347
+ const measured =
348
+ perKeySamples.length >= sampleTurns
349
+ ? percentile(perKeySamples, 0.9)
350
+ : null;
351
+ // The lane's own measurement is what a receipt read costs. Before it has
352
+ // one, a receipt key is priced as a row: the ledger's settled p90 per
353
+ // owner, or the unmeasured prior behind it. That is an overestimate for a
354
+ // claim that returns no output, and deliberately so — the alternative is
355
+ // admitting an unknown payload against a budget it may not fit.
356
+ return measured === null ? occupancy.ownerReserveLedgerBytes : measured;
357
+ };
358
+
359
+ /** Prefer a manifest's exact serialized body size over the conservative prior. */
360
+ const expectedHydrationBytes = (
361
+ waiter: HydrationWaiter,
362
+ occupancy: RuntimeMemoryOccupancy,
363
+ ): number =>
364
+ waiter.expectedBytes === null
365
+ ? Math.max(1, Math.ceil(waiter.receiptKeys * priceKey(occupancy)))
366
+ : Math.max(1, waiter.expectedBytes);
367
+
368
+ /**
369
+ * Live bytes per ledger byte for what this lane actually registers.
370
+ *
371
+ * The lane's reservations land on `hydrationReadBack`, so that is the
372
+ * category whose factor converts them. The process-wide mix-weighted factor
373
+ * is a different question's answer: a run whose ledger is mostly decoded
374
+ * provider bodies reports ~3x, and pricing a receipt read at 3x its wire
375
+ * size holds turns against headroom the read was never going to occupy.
376
+ */
377
+ const hydrationFactor = (occupancy: RuntimeMemoryOccupancy): number =>
378
+ occupancy.categoryFactors.hydrationReadBack;
379
+
380
+ const stopTimer = (): void => {
381
+ if (timer === null) return;
382
+ clearInterval(timer);
383
+ timer = null;
384
+ };
385
+
386
+ const startTimer = (): void => {
387
+ if (timer !== null || closed || waiters.length === 0) return;
388
+ timer = setInterval(() => {
389
+ admitWaiters();
390
+ }, probeIntervalMs);
391
+ if (typeof timer === 'object' && 'unref' in timer) timer.unref();
392
+ };
393
+
394
+ /** Drop a waiter that will never be admitted and settle it loudly. */
395
+ const abandonWaiter = (waiter: HydrationWaiter, reason: unknown): void => {
396
+ const index = waiters.indexOf(waiter);
397
+ if (index >= 0) waiters.splice(index, 1);
398
+ waiter.abandon?.();
399
+ waiter.abandon = null;
400
+ waiter.reject(reason);
401
+ if (waiters.length === 0) stopTimer();
402
+ };
403
+
404
+ const admit = (waiter: HydrationWaiter, reservedBytes: number): void => {
405
+ waiters.shift();
406
+ waiter.admitted = true;
407
+ waiter.abandon?.();
408
+ waiter.abandon = null;
409
+ active += 1;
410
+ turns += 1;
411
+ const waitedMs = Math.max(0, now() - waiter.queuedAtMs);
412
+ totalWaitMs += waitedMs;
413
+ maxWaitMs = Math.max(maxWaitMs, waitedMs);
414
+ const handle = input.reserve(reservedBytes);
415
+ bytesAdmitted += reservedBytes;
416
+ const source = bySource[waiter.source];
417
+ source.turns += 1;
418
+ source.receiptKeys += waiter.receiptKeys;
419
+ source.bytesAdmitted += reservedBytes;
420
+ bytesHeld += reservedBytes;
421
+ bytesHeldPeak = Math.max(bytesHeldPeak, bytesHeld);
422
+ let entryBytes = reservedBytes;
423
+ let settled = false;
424
+ let released = false;
425
+ waiter.resolve({
426
+ settleBytes(bytes) {
427
+ if (released || settled) return;
428
+ if (!Number.isFinite(bytes) || bytes < 0) {
429
+ throw new RangeError(
430
+ `Receipt hydration turn reported ${String(bytes)} bytes; a turn's measured size must be a finite, non-negative byte count.`,
431
+ );
432
+ }
433
+ settled = true;
434
+ const measuredBytes = Math.ceil(bytes);
435
+ handle.update(measuredBytes);
436
+ bytesHeld = Math.max(0, bytesHeld - entryBytes + measuredBytes);
437
+ bytesHeldPeak = Math.max(bytesHeldPeak, bytesHeld);
438
+ bytesMeasured += measuredBytes;
439
+ source.bytesMeasured += measuredBytes;
440
+ entryBytes = measuredBytes;
441
+ recordPerKeySample(
442
+ Math.ceil(measuredBytes / Math.max(1, waiter.receiptKeys)),
443
+ );
444
+ // A turn that turned out smaller than its reservation just freed
445
+ // headroom; hand it to whoever is waiting rather than to the clock.
446
+ admitWaiters();
447
+ },
448
+ release() {
449
+ if (released) return;
450
+ released = true;
451
+ handle.release();
452
+ bytesHeld = Math.max(0, bytesHeld - entryBytes);
453
+ entryBytes = 0;
454
+ active = Math.max(0, active - 1);
455
+ admitWaiters();
456
+ },
457
+ });
458
+ };
459
+
460
+ function admitWaiters(): void {
461
+ if (closed) return;
462
+ while (waiters.length > 0) {
463
+ const waiter = waiters[0];
464
+ if (!waiter) break;
465
+ // One turn may always run. Without this a group whose expected bytes
466
+ // exceed the whole heap target could never start, and the run would
467
+ // stall on a lane that is protecting an empty process.
468
+ if (active === 0) {
469
+ progressAdmissions += 1;
470
+ const occupancy = occupancyReader.read();
471
+ admit(
472
+ waiter,
473
+ occupancy === null ? 0 : expectedHydrationBytes(waiter, occupancy),
474
+ );
475
+ continue;
476
+ }
477
+ let occupancy = occupancyReader.read();
478
+ if (occupancy === null) {
479
+ // No readable heap: this runtime cannot state a budget, so the lane
480
+ // does not invent one. The structural receipt limits still apply.
481
+ unpricedAdmissions += 1;
482
+ admit(waiter, 0);
483
+ continue;
484
+ }
485
+ let reservedBytes = expectedHydrationBytes(waiter, occupancy);
486
+ perKeyBytes = Math.ceil(reservedBytes / Math.max(1, waiter.receiptKeys));
487
+ headroomBytes = occupancy.headroomBytes;
488
+ if (
489
+ reservedBytes * hydrationFactor(occupancy) <=
490
+ occupancy.headroomBytes
491
+ ) {
492
+ admit(waiter, reservedBytes);
493
+ continue;
494
+ }
495
+ // Over budget is a question about garbage first. Collect and re-read
496
+ // before concluding that the turns in flight are what is full.
497
+ const collected = occupancyReader.collectAndRead();
498
+ if (collected) {
499
+ holdCollections += 1;
500
+ occupancy = collected;
501
+ reservedBytes = expectedHydrationBytes(waiter, occupancy);
502
+ perKeyBytes = Math.ceil(
503
+ reservedBytes / Math.max(1, waiter.receiptKeys),
504
+ );
505
+ headroomBytes = occupancy.headroomBytes;
506
+ if (
507
+ reservedBytes * hydrationFactor(occupancy) <=
508
+ occupancy.headroomBytes
509
+ ) {
510
+ admit(waiter, reservedBytes);
511
+ continue;
512
+ }
513
+ }
514
+ holds += 1;
515
+ break;
516
+ }
517
+ if (waiters.length === 0) stopTimer();
518
+ else startTimer();
519
+ }
520
+
521
+ return {
522
+ acquire(request) {
523
+ const requestedKeys = request?.receiptKeys;
524
+ const receiptKeys =
525
+ typeof requestedKeys === 'number' && Number.isFinite(requestedKeys)
526
+ ? Math.max(1, Math.floor(requestedKeys))
527
+ : 1;
528
+ const source =
529
+ request?.source &&
530
+ RUNTIME_RECEIPT_HYDRATION_SOURCES.includes(request.source)
531
+ ? request.source
532
+ : 'unknown';
533
+ const expectedBytes =
534
+ typeof request?.expectedBytes === 'number' &&
535
+ Number.isFinite(request.expectedBytes) &&
536
+ request.expectedBytes >= 0
537
+ ? Math.ceil(request.expectedBytes)
538
+ : null;
539
+ const timeoutMs =
540
+ typeof request?.timeoutMs === 'number' &&
541
+ Number.isFinite(request.timeoutMs) &&
542
+ request.timeoutMs >= 0
543
+ ? Math.floor(request.timeoutMs)
544
+ : null;
545
+ const signal = request?.signal;
546
+ if (closed) {
547
+ return Promise.reject(new RuntimeReceiptHydrationGateClosedError());
548
+ }
549
+ if (signal?.aborted) {
550
+ return Promise.reject(hydrationAbortReason(signal));
551
+ }
552
+ return new Promise<RuntimeReceiptHydrationTurn>((resolve, reject) => {
553
+ const queuedAtMs = now();
554
+ const waiter: HydrationWaiter = {
555
+ receiptKeys,
556
+ expectedBytes,
557
+ source,
558
+ queuedAtMs,
559
+ admitted: false,
560
+ resolve,
561
+ reject,
562
+ abandon: null,
563
+ };
564
+ waiters.push(waiter);
565
+ let timer: ReturnType<typeof setTimeout> | null = null;
566
+ const onAbort = () =>
567
+ abandonWaiter(waiter, hydrationAbortReason(signal!));
568
+ if (timeoutMs !== null || signal) {
569
+ if (timeoutMs !== null) {
570
+ timer = setTimeout(() => {
571
+ timer = null;
572
+ abandonWaiter(
573
+ waiter,
574
+ new RuntimeReceiptHydrationTurnTimeoutError(
575
+ Math.max(0, now() - queuedAtMs),
576
+ ),
577
+ );
578
+ }, timeoutMs);
579
+ if (typeof timer === 'object' && 'unref' in timer) timer.unref();
580
+ }
581
+ signal?.addEventListener('abort', onAbort, { once: true });
582
+ waiter.abandon = () => {
583
+ if (timer !== null) clearTimeout(timer);
584
+ timer = null;
585
+ signal?.removeEventListener('abort', onAbort);
586
+ };
587
+ }
588
+ admitWaiters();
589
+ // Counted only when the byte budget actually made this turn wait, so
590
+ // `queuedTurns / turns` stays the share of turns the lane delayed.
591
+ if (!waiter.admitted) queuedTurns += 1;
592
+ });
593
+ },
594
+ snapshot: () => ({
595
+ active,
596
+ waiting: waiters.length,
597
+ turns,
598
+ queuedTurns,
599
+ totalWaitMs,
600
+ maxWaitMs,
601
+ bytesHeld,
602
+ bytesHeldPeak,
603
+ bytesAdmitted,
604
+ bytesMeasured,
605
+ holds,
606
+ holdCollections,
607
+ progressAdmissions,
608
+ unpricedAdmissions,
609
+ perKeyBytes,
610
+ perKeySamples: perKeySamples.length,
611
+ headroomBytes,
612
+ bySource: Object.fromEntries(
613
+ RUNTIME_RECEIPT_HYDRATION_SOURCES.map((source) => [
614
+ source,
615
+ { ...bySource[source] },
616
+ ]),
617
+ ) as Record<
618
+ RuntimeReceiptHydrationSource,
619
+ RuntimeReceiptHydrationSourceSnapshot
620
+ >,
621
+ }),
622
+ close() {
623
+ closed = true;
624
+ stopTimer();
625
+ // A closed lane will never admit again. Stranding queued turns on an
626
+ // unsettleable promise is the wait this contract forbids.
627
+ const stranded = waiters.splice(0, waiters.length);
628
+ for (const waiter of stranded) {
629
+ waiter.abandon?.();
630
+ waiter.abandon = null;
631
+ waiter.reject(new RuntimeReceiptHydrationGateClosedError());
632
+ }
633
+ },
634
+ };
635
+ }
636
+
637
+ /**
638
+ * Serialized size of the receipt outputs a hydration turn is about to expand
639
+ * and hold. Only stored outputs are measured: a claim that returned no output
640
+ * costs the lane nothing but its own row, and serializing the receipt envelope
641
+ * would charge the lane for bytes it does not retain.
642
+ */
643
+ export function runtimeStepReceiptPayloadBytes(
644
+ receipts: Iterable<RuntimeStepReceipt | null | undefined>,
645
+ ): number {
646
+ let bytes = 0;
647
+ for (const receipt of receipts) {
648
+ const output = receipt?.output;
649
+ if (output === undefined || output === null) continue;
650
+ bytes += runtimeJsonByteLength(output);
651
+ }
652
+ return bytes;
653
+ }
654
+
33
655
  const TOOL_RECEIPT_DEFAULT_WAIT_MS = 300_000;
34
656
  const TOOL_RECEIPT_COMPLETION_BUFFER_MS = 30_000;
35
657
  const TOOL_RECEIPT_MAX_WAIT_MS = 30 * 60_000;
@@ -37,6 +659,32 @@ const DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS = 15_000;
37
659
  export const COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID =
38
660
  '__deepline_completed_cache_insert__';
39
661
 
662
+ /** The error an aborted queue wait settles with. */
663
+ function hydrationAbortReason(signal: AbortSignal): unknown {
664
+ if (signal.reason instanceof Error) return signal.reason;
665
+ return new Error(
666
+ typeof signal.reason === 'string' && signal.reason.trim()
667
+ ? signal.reason
668
+ : 'Receipt hydration turn aborted.',
669
+ );
670
+ }
671
+
672
+ export class RuntimeReceiptHydrationTurnTimeoutError extends Error {
673
+ constructor(readonly waitedMs: number) {
674
+ super(
675
+ `Timed out after ${waitedMs}ms waiting for a receipt hydration turn.`,
676
+ );
677
+ this.name = 'RuntimeReceiptHydrationTurnTimeoutError';
678
+ }
679
+ }
680
+
681
+ export class RuntimeReceiptHydrationGateClosedError extends Error {
682
+ constructor() {
683
+ super('Receipt hydration lane closed while a turn was still queued.');
684
+ this.name = 'RuntimeReceiptHydrationGateClosedError';
685
+ }
686
+ }
687
+
40
688
  export class RuntimeReceiptWaitTimeoutError extends Error {
41
689
  constructor(key: string) {
42
690
  super(`Timed out waiting for durable receipt ${key}.`);
@@ -520,6 +1168,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
520
1168
  );
521
1169
  }
522
1170
  const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
1171
+ let completedWaitAttempt = 0;
523
1172
  while (!ownsLock) {
524
1173
  const lock = await input.store.acquireExecutionLock({
525
1174
  receiptKey: input.receiptKey,
@@ -538,7 +1187,13 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
538
1187
  if (Date.now() >= deadline) {
539
1188
  throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
540
1189
  }
541
- await sleepReceiptWait(DURABLE_RECEIPT_WAIT_DELAY_MS);
1190
+ completedWaitAttempt += 1;
1191
+ await sleepReceiptWait(
1192
+ Math.min(
1193
+ runtimeReceiptWaitDelayMs(completedWaitAttempt),
1194
+ Math.max(0, deadline - Date.now()),
1195
+ ),
1196
+ );
542
1197
  }
543
1198
  if (input.force !== true) {
544
1199
  const afterLock = await inspectCompletedReceipt(
@@ -616,33 +1271,30 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
616
1271
  if (input.force === true) {
617
1272
  return result;
618
1273
  }
619
- // Completion normally returns a compact acknowledgement. Keep the
620
- // mutation and response decode inside the hydration turn anyway because
621
- // a concurrent winner may return a full persisted payload.
622
- const published = await withCompletedReceiptHydration(async () => {
623
- const completed = await input.store.complete(
624
- input.receiptKey,
625
- input.runId,
626
- isToolExecuteResult(result)
627
- ? serializeToolExecuteResult(result)
628
- : result,
629
- COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
1274
+ // The hydration window bounds how many stored payloads expand at once.
1275
+ // A completion is a write, and on wire format 3 it answers with a
1276
+ // compact acknowledgement carrying no payload at all — nothing expands,
1277
+ // so nothing needs a turn. Holding a slot of the 8-wide run-global
1278
+ // window across the whole gateway round trip only queued unrelated rows
1279
+ // behind this row's write. Only a response that really carries a stored
1280
+ // payload decodes under the gate.
1281
+ const completed = await input.store.complete(
1282
+ input.receiptKey,
1283
+ input.runId,
1284
+ isToolExecuteResult(result)
1285
+ ? serializeToolExecuteResult(result)
1286
+ : result,
1287
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
1288
+ );
1289
+ if (
1290
+ completed?.status === 'completed' ||
1291
+ completed?.status === 'skipped'
1292
+ ) {
1293
+ if (completed.output === undefined) return result;
1294
+ return await withCompletedReceiptHydration(
1295
+ async () => await recoverCompletedReceipt(completed, 'owner'),
630
1296
  );
631
- if (
632
- completed?.status === 'completed' ||
633
- completed?.status === 'skipped'
634
- ) {
635
- return {
636
- kind: 'completed' as const,
637
- output:
638
- completed.output === undefined
639
- ? result
640
- : await recoverCompletedReceipt(completed, 'owner'),
641
- };
642
- }
643
- return { kind: 'unresolved' as const };
644
- });
645
- if (published.kind === 'completed') return published.output;
1297
+ }
646
1298
  const winner = await inspectCompletedReceipt(
647
1299
  () => input.store.get(input.receiptKey),
648
1300
  'owner',
@@ -704,8 +1356,22 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
704
1356
  input.runningReceiptWaitMaxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
705
1357
  const delayMs =
706
1358
  input.runningReceiptWaitDelayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
1359
+ // An explicit delay keeps the caller's fixed cadence; the default backs off.
1360
+ const backoff = input.runningReceiptWaitDelayMs == null;
1361
+ const waitDeadline =
1362
+ Date.now() + runtimeReceiptWaitBudgetMs(maxAttempts, delayMs);
707
1363
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
708
- if (attempt > 0) await sleepReceiptWait(delayMs);
1364
+ if (attempt > 0) {
1365
+ if (backoff && Date.now() >= waitDeadline) break;
1366
+ await sleepReceiptWait(
1367
+ backoff
1368
+ ? Math.min(
1369
+ runtimeReceiptWaitDelayMs(attempt),
1370
+ Math.max(0, waitDeadline - Date.now()),
1371
+ )
1372
+ : delayMs,
1373
+ );
1374
+ }
709
1375
  const current = await input.store.get(input.receiptKey);
710
1376
  if (current?.status === 'completed' || current?.status === 'skipped') {
711
1377
  return {