deepline 0.1.251 → 0.1.253
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.
- package/dist/bundling-sources/sdk/src/release.ts +5 -2
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +79 -64
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +37 -35
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +20 -5
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +462 -29
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +7 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +98 -13
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +117 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +19 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +22 -0
- package/dist/cli/index.js +47 -23
- package/dist/cli/index.mjs +47 -23
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -5
- package/dist/index.mjs +10 -5
- package/package.json +2 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The durable terminal envelope carries the complete 5 MiB authored return
|
|
3
|
+
* plus bounded scheduler-owned metadata (progress, warnings, and a log tail).
|
|
4
|
+
* It must not make a return that passed the customer contract fail later.
|
|
5
|
+
*/
|
|
6
|
+
export const TERMINAL_RUN_RESULT_MAX_BYTES = 6 * 1024 * 1024;
|
|
4
7
|
/**
|
|
5
8
|
* Convex hard-caps a single document at 1 MiB and rejects nesting deeper than
|
|
6
9
|
* 16 levels. A run-ledger `run.completed` / `run.failed` event embeds its
|
|
@@ -23,40 +26,191 @@ export const LEDGER_TERMINAL_RESULT_MAX_DEPTH = 10;
|
|
|
23
26
|
export const CONVEX_LEDGER_ARRAY_MAX_ITEMS = 8_192;
|
|
24
27
|
export const CONVEX_LEDGER_OBJECT_MAX_FIELDS = 1_024;
|
|
25
28
|
export const CONVEX_LEDGER_FIELD_NAME_MAX_LENGTH = 1_024;
|
|
26
|
-
|
|
27
|
-
export const
|
|
29
|
+
/** Customer-visible values and complete authored returns share one 5 MiB cap. */
|
|
30
|
+
export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 5 * 1024 * 1024;
|
|
31
|
+
export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
28
32
|
/** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
|
|
29
33
|
export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
|
|
30
34
|
/** One completion request may contain multiple receipts up to 32 MiB total. */
|
|
31
35
|
export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
36
|
+
/**
|
|
37
|
+
* The Fly receipt gateway accepts one runner-terminal control request up to
|
|
38
|
+
* this size. This is a transport ceiling, not a customer-output contract:
|
|
39
|
+
* settled customer returns remain capped at 5 MiB and the 6 MiB durable
|
|
40
|
+
* terminal envelope; a suspended checkpoint is retained separately because it
|
|
41
|
+
* is required to resume execution.
|
|
42
|
+
*/
|
|
43
|
+
export const RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
44
|
+
/**
|
|
45
|
+
* The terminal `result` line is the crash-recovery record in Daytona stdout.
|
|
46
|
+
* Once that line is written, retries may still emit runner-owned diagnostics.
|
|
47
|
+
* Keep their aggregate UTF-8 footprint below this allowance so a gateway-size
|
|
48
|
+
* suspended checkpoint remains inside the crash pusher's bounded tail read.
|
|
49
|
+
*/
|
|
50
|
+
export const RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES = 64 * 1024;
|
|
32
51
|
|
|
33
52
|
export class OutputTooLargeError extends Error {
|
|
34
53
|
readonly code = 'OUTPUT_TOO_LARGE';
|
|
35
54
|
readonly path: string;
|
|
36
55
|
readonly bytes: number;
|
|
37
56
|
readonly limitBytes: number;
|
|
57
|
+
readonly measuredExactly: boolean;
|
|
38
58
|
|
|
39
59
|
constructor(input: {
|
|
40
60
|
path: string;
|
|
41
61
|
bytes: number;
|
|
42
62
|
limitBytes: number;
|
|
43
63
|
advice: string;
|
|
64
|
+
measuredExactly?: boolean;
|
|
44
65
|
}) {
|
|
66
|
+
const sizeDescription = input.measuredExactly === false ? 'at least ' : '';
|
|
45
67
|
super(
|
|
46
|
-
`
|
|
68
|
+
`OUTPUT_TOO_LARGE: ${input.path} is ${sizeDescription}${formatBytes(input.bytes)}, above the ${formatBytes(input.limitBytes)} limit. ` +
|
|
47
69
|
input.advice,
|
|
48
70
|
);
|
|
49
71
|
this.name = 'OutputTooLargeError';
|
|
50
72
|
this.path = input.path;
|
|
51
73
|
this.bytes = input.bytes;
|
|
52
74
|
this.limitBytes = input.limitBytes;
|
|
75
|
+
this.measuredExactly = input.measuredExactly !== false;
|
|
53
76
|
}
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
export function jsonByteLength(value: unknown): number {
|
|
57
80
|
const serialized = JSON.stringify(value);
|
|
58
81
|
if (serialized === undefined) return 0;
|
|
59
|
-
return
|
|
82
|
+
return utf8ByteLength(serialized);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type BoundedJsonByteLength = {
|
|
86
|
+
bytes: number;
|
|
87
|
+
exceeded: boolean;
|
|
88
|
+
exact: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const JSON_SIZE_LIMIT_REACHED = Symbol('JSON_SIZE_LIMIT_REACHED');
|
|
92
|
+
|
|
93
|
+
function utf8ByteLength(value: string): number {
|
|
94
|
+
let bytes = 0;
|
|
95
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
96
|
+
const code = value.charCodeAt(index);
|
|
97
|
+
if (code <= 0x7f) {
|
|
98
|
+
bytes += 1;
|
|
99
|
+
} else if (code <= 0x7ff) {
|
|
100
|
+
bytes += 2;
|
|
101
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
102
|
+
const next = value.charCodeAt(index + 1);
|
|
103
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
104
|
+
bytes += 4;
|
|
105
|
+
index += 1;
|
|
106
|
+
} else {
|
|
107
|
+
bytes += 3;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
bytes += 3;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return bytes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function jsonStringByteLength(value: string): number {
|
|
117
|
+
let bytes = 2;
|
|
118
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
119
|
+
const code = value.charCodeAt(index);
|
|
120
|
+
if (code === 0x22 || code === 0x5c) {
|
|
121
|
+
bytes += 2;
|
|
122
|
+
} else if (code <= 0x1f) {
|
|
123
|
+
bytes +=
|
|
124
|
+
code === 0x08 ||
|
|
125
|
+
code === 0x09 ||
|
|
126
|
+
code === 0x0a ||
|
|
127
|
+
code === 0x0c ||
|
|
128
|
+
code === 0x0d
|
|
129
|
+
? 2
|
|
130
|
+
: 6;
|
|
131
|
+
} else if (code <= 0x7f) {
|
|
132
|
+
bytes += 1;
|
|
133
|
+
} else if (code <= 0x7ff) {
|
|
134
|
+
bytes += 2;
|
|
135
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
136
|
+
const next = value.charCodeAt(index + 1);
|
|
137
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
138
|
+
bytes += 4;
|
|
139
|
+
index += 1;
|
|
140
|
+
} else {
|
|
141
|
+
bytes += 6;
|
|
142
|
+
}
|
|
143
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
144
|
+
bytes += 6;
|
|
145
|
+
} else {
|
|
146
|
+
bytes += 3;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return bytes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Measure JSON only up to a caller-owned ceiling. The replacer accounts for a
|
|
154
|
+
* lower bound before JSON.stringify builds its result and aborts as soon as
|
|
155
|
+
* that bound crosses the limit. Oversized strings are scanned, never copied.
|
|
156
|
+
*/
|
|
157
|
+
export function jsonByteLengthUpTo(
|
|
158
|
+
value: unknown,
|
|
159
|
+
limitBytes: number,
|
|
160
|
+
): BoundedJsonByteLength {
|
|
161
|
+
let observedBytes = 0;
|
|
162
|
+
const add = (bytes: number) => {
|
|
163
|
+
observedBytes += bytes;
|
|
164
|
+
if (observedBytes > limitBytes) throw JSON_SIZE_LIMIT_REACHED;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const serialized = JSON.stringify(
|
|
169
|
+
value,
|
|
170
|
+
function (this: unknown, key, entry: unknown) {
|
|
171
|
+
const arrayParent = Array.isArray(this);
|
|
172
|
+
const omittedObjectEntry =
|
|
173
|
+
!arrayParent &&
|
|
174
|
+
(entry === undefined ||
|
|
175
|
+
typeof entry === 'function' ||
|
|
176
|
+
typeof entry === 'symbol');
|
|
177
|
+
if (key && !arrayParent && !omittedObjectEntry) {
|
|
178
|
+
add(jsonStringByteLength(key) + 1);
|
|
179
|
+
}
|
|
180
|
+
if (typeof entry === 'string') {
|
|
181
|
+
add(jsonStringByteLength(entry));
|
|
182
|
+
} else if (typeof entry === 'number') {
|
|
183
|
+
add(Number.isFinite(entry) ? String(entry).length : 4);
|
|
184
|
+
} else if (typeof entry === 'boolean') {
|
|
185
|
+
add(entry ? 4 : 5);
|
|
186
|
+
} else if (entry === null) {
|
|
187
|
+
add(4);
|
|
188
|
+
} else if (
|
|
189
|
+
arrayParent &&
|
|
190
|
+
(entry === undefined ||
|
|
191
|
+
typeof entry === 'function' ||
|
|
192
|
+
typeof entry === 'symbol')
|
|
193
|
+
) {
|
|
194
|
+
add(4);
|
|
195
|
+
} else if (entry && typeof entry === 'object') {
|
|
196
|
+
// At least one opening delimiter. This makes deeply repeated empty
|
|
197
|
+
// containers advance the bound even before punctuation is counted.
|
|
198
|
+
add(1);
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
if (serialized === undefined) {
|
|
204
|
+
return { bytes: 0, exceeded: false, exact: true };
|
|
205
|
+
}
|
|
206
|
+
const bytes = utf8ByteLength(serialized);
|
|
207
|
+
return { bytes, exceeded: bytes > limitBytes, exact: true };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error === JSON_SIZE_LIMIT_REACHED) {
|
|
210
|
+
return { bytes: observedBytes, exceeded: true, exact: false };
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
60
214
|
}
|
|
61
215
|
|
|
62
216
|
function formatBytes(bytes: number): string {
|
|
@@ -75,9 +229,9 @@ function assertJsonSized(input: {
|
|
|
75
229
|
limitBytes: number;
|
|
76
230
|
advice: string;
|
|
77
231
|
}): number {
|
|
78
|
-
let
|
|
232
|
+
let measurement: BoundedJsonByteLength;
|
|
79
233
|
try {
|
|
80
|
-
|
|
234
|
+
measurement = jsonByteLengthUpTo(input.value, input.limitBytes);
|
|
81
235
|
} catch (error) {
|
|
82
236
|
throw new Error(
|
|
83
237
|
`${input.path} cannot be serialized as JSON: ${
|
|
@@ -85,22 +239,23 @@ function assertJsonSized(input: {
|
|
|
85
239
|
}`,
|
|
86
240
|
);
|
|
87
241
|
}
|
|
88
|
-
if (
|
|
242
|
+
if (measurement.exceeded) {
|
|
89
243
|
throw new OutputTooLargeError({
|
|
90
244
|
path: input.path,
|
|
91
|
-
bytes,
|
|
245
|
+
bytes: measurement.bytes,
|
|
92
246
|
limitBytes: input.limitBytes,
|
|
93
247
|
advice: input.advice,
|
|
248
|
+
measuredExactly: measurement.exact,
|
|
94
249
|
});
|
|
95
250
|
}
|
|
96
|
-
return bytes;
|
|
251
|
+
return measurement.bytes;
|
|
97
252
|
}
|
|
98
253
|
|
|
99
254
|
export function valueWithinJsonByteLimit(value: unknown, limitBytes: number) {
|
|
100
255
|
if (value === undefined || value === null) {
|
|
101
256
|
return value;
|
|
102
257
|
}
|
|
103
|
-
return
|
|
258
|
+
return jsonByteLengthUpTo(value, limitBytes).exceeded ? undefined : value;
|
|
104
259
|
}
|
|
105
260
|
|
|
106
261
|
/** Shared terminal-result contract for every scheduler backend. */
|
|
@@ -108,6 +263,18 @@ export function persistableTerminalRunResult(value: unknown): unknown {
|
|
|
108
263
|
return valueWithinJsonByteLimit(value, TERMINAL_RUN_RESULT_MAX_BYTES);
|
|
109
264
|
}
|
|
110
265
|
|
|
266
|
+
/** A completed run must never commit without its complete terminal value. */
|
|
267
|
+
export function assertTerminalRunResultWithinLimit(value: unknown): unknown {
|
|
268
|
+
assertJsonSized({
|
|
269
|
+
value,
|
|
270
|
+
path: 'terminal run result',
|
|
271
|
+
limitBytes: TERMINAL_RUN_RESULT_MAX_BYTES,
|
|
272
|
+
advice:
|
|
273
|
+
'The complete terminal result cannot be persisted. Keep authored output within its documented limits and keep transport metadata bounded.',
|
|
274
|
+
});
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
111
278
|
/**
|
|
112
279
|
* Replay-only keys a terminal `PlayRunnerResult` carries for the scheduler
|
|
113
280
|
* (Postgres) data plane. These are NEVER read back from the Convex run ledger:
|
|
@@ -129,10 +296,26 @@ export type LedgerTerminalResultRef = {
|
|
|
129
296
|
/** Column on the scheduler `work_runs` row that holds the full result. */
|
|
130
297
|
key: 'terminal_result_json';
|
|
131
298
|
/** Serialized byte size of the result that was elided from the ledger. */
|
|
132
|
-
bytes
|
|
299
|
+
bytes?: number;
|
|
133
300
|
reason: LedgerTerminalResultRefReason;
|
|
301
|
+
/** The live Convex projection was intentionally replaced, not lost. */
|
|
302
|
+
projection?: 'out_of_line';
|
|
303
|
+
/** New refs point only at a value that was actually persisted in full. */
|
|
304
|
+
content?: 'full';
|
|
305
|
+
/** Bounded customer-safe explanation; contains no payload paths or keys. */
|
|
306
|
+
warning?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Compact, Convex-safe run-list projection captured while the terminal
|
|
309
|
+
* result is already in memory. List/live polling reads this instead of
|
|
310
|
+
* fetching the complete scheduler result.
|
|
311
|
+
*/
|
|
312
|
+
preview?: BoundedRunListOutputPreview;
|
|
134
313
|
};
|
|
135
314
|
|
|
315
|
+
export const LEDGER_TERMINAL_RESULT_STORED_WARNING =
|
|
316
|
+
'The terminal result is stored outside the live run projection because it exceeded a bounded storage limit. The run status and persisted row data are unaffected.';
|
|
317
|
+
export const LEDGER_TERMINAL_RESULT_OMITTED_WARNING =
|
|
318
|
+
'The terminal result was omitted from the live run projection because it exceeded a bounded storage limit. The run status and persisted row data are unaffected.';
|
|
136
319
|
export type LedgerTerminalResultRefReason =
|
|
137
320
|
| 'terminal_result_exceeds_ledger_limit'
|
|
138
321
|
| 'terminal_result_exceeds_ledger_depth'
|
|
@@ -159,6 +342,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
159
342
|
|
|
160
343
|
function isConvexFieldName(value: string): boolean {
|
|
161
344
|
if (
|
|
345
|
+
value.length === 0 ||
|
|
162
346
|
value.length > CONVEX_LEDGER_FIELD_NAME_MAX_LENGTH ||
|
|
163
347
|
value.startsWith('$')
|
|
164
348
|
) {
|
|
@@ -171,6 +355,201 @@ function isConvexFieldName(value: string): boolean {
|
|
|
171
355
|
return true;
|
|
172
356
|
}
|
|
173
357
|
|
|
358
|
+
function isSafeRunListPreviewFieldName(value: string): boolean {
|
|
359
|
+
return (
|
|
360
|
+
isConvexFieldName(value) &&
|
|
361
|
+
value !== '__proto__' &&
|
|
362
|
+
value !== 'constructor' &&
|
|
363
|
+
value !== 'prototype'
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The dashboard's historical run list is a polling surface. Keep its output
|
|
369
|
+
* projection tiny even when the authored return is the full 5 MiB allowed by
|
|
370
|
+
* the customer contract. These limits deliberately mirror the API response
|
|
371
|
+
* budget, but live here so the durable Convex summary can be written without
|
|
372
|
+
* importing a server-only route helper.
|
|
373
|
+
*/
|
|
374
|
+
export const RUN_LIST_OUTPUT_PREVIEW_LIMITS = {
|
|
375
|
+
maxFields: 40,
|
|
376
|
+
// The list endpoint returns multiple historical runs in one polling page.
|
|
377
|
+
// Reserve enough room for run state and legacy rows by keeping each preview
|
|
378
|
+
// at 4 KiB; the complete authored result remains separately readable up to
|
|
379
|
+
// the 5 MiB customer-output ceiling.
|
|
380
|
+
maxBytes: 4_000,
|
|
381
|
+
maxDepth: 4,
|
|
382
|
+
maxStringLength: 300,
|
|
383
|
+
maxArrayItems: 5,
|
|
384
|
+
maxObjectFields: 40,
|
|
385
|
+
} as const;
|
|
386
|
+
|
|
387
|
+
export type BoundedRunListOutputPreview = {
|
|
388
|
+
output: Record<string, unknown>;
|
|
389
|
+
truncated: boolean;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
function compactRunListPreviewString(value: string): {
|
|
393
|
+
value: string;
|
|
394
|
+
truncated: boolean;
|
|
395
|
+
} {
|
|
396
|
+
const compact = value.replace(/\s+/g, ' ').trim();
|
|
397
|
+
if (compact.length <= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxStringLength) {
|
|
398
|
+
return { value: compact, truncated: compact !== value };
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
value: `${compact.slice(0, RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxStringLength)}...`,
|
|
402
|
+
truncated: true,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Produce a small projection that satisfies Convex's field-name/depth rules.
|
|
408
|
+
* Recursion stops at a small, caller-owned depth so an unsafe result can never
|
|
409
|
+
* make its own preview projection exhaust the stack.
|
|
410
|
+
*/
|
|
411
|
+
function compactRunListPreviewValue(
|
|
412
|
+
value: unknown,
|
|
413
|
+
depth: number,
|
|
414
|
+
): { value: unknown; truncated: boolean } {
|
|
415
|
+
if (
|
|
416
|
+
value == null ||
|
|
417
|
+
typeof value === 'number' ||
|
|
418
|
+
typeof value === 'boolean'
|
|
419
|
+
) {
|
|
420
|
+
return { value, truncated: false };
|
|
421
|
+
}
|
|
422
|
+
if (typeof value === 'string') {
|
|
423
|
+
return compactRunListPreviewString(value);
|
|
424
|
+
}
|
|
425
|
+
if (depth >= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxDepth) {
|
|
426
|
+
return {
|
|
427
|
+
value: Array.isArray(value) ? '[Array]' : '[Object]',
|
|
428
|
+
truncated: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (Array.isArray(value)) {
|
|
432
|
+
let truncated = value.length > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxArrayItems;
|
|
433
|
+
const items: unknown[] = [];
|
|
434
|
+
for (const item of value.slice(
|
|
435
|
+
0,
|
|
436
|
+
RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxArrayItems,
|
|
437
|
+
)) {
|
|
438
|
+
const compacted = compactRunListPreviewValue(item, depth + 1);
|
|
439
|
+
truncated ||= compacted.truncated;
|
|
440
|
+
items.push(compacted.value);
|
|
441
|
+
}
|
|
442
|
+
return { value: items, truncated };
|
|
443
|
+
}
|
|
444
|
+
if (!isPlainObject(value)) {
|
|
445
|
+
return { value: String(value), truncated: true };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const output: Record<string, unknown> = Object.create(null) as Record<
|
|
449
|
+
string,
|
|
450
|
+
unknown
|
|
451
|
+
>;
|
|
452
|
+
let truncated = false;
|
|
453
|
+
const entries = Object.entries(value);
|
|
454
|
+
for (const [key, child] of entries.slice(
|
|
455
|
+
0,
|
|
456
|
+
RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxObjectFields,
|
|
457
|
+
)) {
|
|
458
|
+
if (!isSafeRunListPreviewFieldName(key)) {
|
|
459
|
+
truncated = true;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const compacted = compactRunListPreviewValue(child, depth + 1);
|
|
463
|
+
truncated ||= compacted.truncated;
|
|
464
|
+
output[key] = compacted.value;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
value: output,
|
|
468
|
+
truncated:
|
|
469
|
+
truncated ||
|
|
470
|
+
entries.length > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxObjectFields,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function runListPreviewFieldBytes(
|
|
475
|
+
key: string,
|
|
476
|
+
value: unknown,
|
|
477
|
+
hasPreviousField: boolean,
|
|
478
|
+
): number {
|
|
479
|
+
try {
|
|
480
|
+
return utf8ByteLength(
|
|
481
|
+
`${hasPreviousField ? ',' : ''}${JSON.stringify(key)}:${JSON.stringify(value)}`,
|
|
482
|
+
);
|
|
483
|
+
} catch {
|
|
484
|
+
return Number.POSITIVE_INFINITY;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Build the durable run-list preview while the terminal result is already in
|
|
490
|
+
* memory. It never reads a full result from Postgres, and it does not copy
|
|
491
|
+
* invalid Convex field names into the summary projection.
|
|
492
|
+
*/
|
|
493
|
+
export function buildBoundedRunListOutputPreview(
|
|
494
|
+
value: Record<string, unknown>,
|
|
495
|
+
): BoundedRunListOutputPreview {
|
|
496
|
+
const output: Record<string, unknown> = Object.create(null) as Record<
|
|
497
|
+
string,
|
|
498
|
+
unknown
|
|
499
|
+
>;
|
|
500
|
+
let outputBytes = 2;
|
|
501
|
+
let outputFields = 0;
|
|
502
|
+
let truncated = false;
|
|
503
|
+
for (const [key, child] of Object.entries(value)) {
|
|
504
|
+
if (outputFields >= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxFields) {
|
|
505
|
+
truncated = true;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
if (!isSafeRunListPreviewFieldName(key)) {
|
|
509
|
+
truncated = true;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const compacted = compactRunListPreviewValue(child, 0);
|
|
513
|
+
truncated ||= compacted.truncated;
|
|
514
|
+
let previewValue = compacted.value;
|
|
515
|
+
let fieldBytes = runListPreviewFieldBytes(
|
|
516
|
+
key,
|
|
517
|
+
previewValue,
|
|
518
|
+
outputFields > 0,
|
|
519
|
+
);
|
|
520
|
+
if (outputBytes + fieldBytes > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxBytes) {
|
|
521
|
+
truncated = true;
|
|
522
|
+
previewValue = '[Preview omitted; open the run for complete output]';
|
|
523
|
+
fieldBytes = runListPreviewFieldBytes(
|
|
524
|
+
key,
|
|
525
|
+
previewValue,
|
|
526
|
+
outputFields > 0,
|
|
527
|
+
);
|
|
528
|
+
if (outputBytes + fieldBytes > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxBytes) {
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
output[key] = previewValue;
|
|
533
|
+
outputBytes += fieldBytes;
|
|
534
|
+
outputFields += 1;
|
|
535
|
+
}
|
|
536
|
+
return { output, truncated };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Read the preview that is safe to carry in Convex run summaries. Older refs
|
|
541
|
+
* did not persist a preview; mark those as truncated so callers can link to
|
|
542
|
+
* the explicit full-run read without falling back to an unbounded hydration.
|
|
543
|
+
*/
|
|
544
|
+
export function runListOutputPreviewForLedger(
|
|
545
|
+
value: unknown,
|
|
546
|
+
): BoundedRunListOutputPreview | null {
|
|
547
|
+
if (isLedgerTerminalResultRef(value)) {
|
|
548
|
+
return value.preview ?? { output: {}, truncated: true };
|
|
549
|
+
}
|
|
550
|
+
return isPlainObject(value) ? buildBoundedRunListOutputPreview(value) : null;
|
|
551
|
+
}
|
|
552
|
+
|
|
174
553
|
/**
|
|
175
554
|
* Validate the part of Convex's value contract that can permanently poison a
|
|
176
555
|
* durable run-ledger append. The walk is iterative and stops at the depth
|
|
@@ -228,9 +607,12 @@ export function inspectConvexLedgerPayload(
|
|
|
228
607
|
}
|
|
229
608
|
|
|
230
609
|
try {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
610
|
+
const measurement = jsonByteLengthUpTo(
|
|
611
|
+
value,
|
|
612
|
+
LEDGER_TERMINAL_RESULT_MAX_BYTES,
|
|
613
|
+
);
|
|
614
|
+
return measurement.exceeded
|
|
615
|
+
? { reason: 'size_limit', bytes: measurement.bytes }
|
|
234
616
|
: null;
|
|
235
617
|
} catch {
|
|
236
618
|
return { reason: 'not_json_serializable' };
|
|
@@ -281,24 +663,52 @@ function refReasonForIssue(
|
|
|
281
663
|
}
|
|
282
664
|
}
|
|
283
665
|
|
|
666
|
+
function ledgerTerminalResultRef(
|
|
667
|
+
reason: LedgerTerminalResultRefReason,
|
|
668
|
+
options: {
|
|
669
|
+
content?: 'full';
|
|
670
|
+
bytes?: number;
|
|
671
|
+
preview?: BoundedRunListOutputPreview;
|
|
672
|
+
} = {},
|
|
673
|
+
): LedgerTerminalResultRef {
|
|
674
|
+
return {
|
|
675
|
+
__kind: 'deepline.ledger_terminal_result_ref.v1',
|
|
676
|
+
store: 'scheduler_postgres',
|
|
677
|
+
key: 'terminal_result_json',
|
|
678
|
+
...(options.bytes !== undefined ? { bytes: options.bytes } : {}),
|
|
679
|
+
reason,
|
|
680
|
+
projection: 'out_of_line',
|
|
681
|
+
...(options.content ? { content: options.content } : {}),
|
|
682
|
+
...(options.preview ? { preview: options.preview } : {}),
|
|
683
|
+
warning: options.content
|
|
684
|
+
? LEDGER_TERMINAL_RESULT_STORED_WARNING
|
|
685
|
+
: LEDGER_TERMINAL_RESULT_OMITTED_WARNING,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
284
689
|
export function ledgerTerminalResultRefForValue(
|
|
285
690
|
value: unknown,
|
|
286
691
|
reason: LedgerTerminalResultRefReason,
|
|
692
|
+
options: { content?: 'full' } = {},
|
|
287
693
|
): LedgerTerminalResultRef {
|
|
288
|
-
let bytes
|
|
694
|
+
let bytes: number | undefined;
|
|
289
695
|
try {
|
|
290
|
-
bytes =
|
|
696
|
+
bytes = jsonByteLengthUpTo(value, LEDGER_TERMINAL_RESULT_MAX_BYTES).bytes;
|
|
291
697
|
} catch {
|
|
292
698
|
// The bounded reason is the useful diagnostic. Never serialize the
|
|
293
699
|
// customer payload again merely to populate reference metadata.
|
|
294
700
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
701
|
+
let preview: BoundedRunListOutputPreview | undefined;
|
|
702
|
+
if (options.content && isPlainObject(value)) {
|
|
703
|
+
try {
|
|
704
|
+
preview = buildBoundedRunListOutputPreview(value);
|
|
705
|
+
} catch {
|
|
706
|
+
// The terminal reference is the durable fallback. A hostile/non-JSON
|
|
707
|
+
// value must not turn optional list-preview derivation into a terminal
|
|
708
|
+
// persistence failure.
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return ledgerTerminalResultRef(reason, { ...options, bytes, preview });
|
|
302
712
|
}
|
|
303
713
|
|
|
304
714
|
/** Optional derived summaries must never block the terminal lifecycle event. */
|
|
@@ -317,10 +727,14 @@ export function ledgerSummaryForLedger(value: unknown): unknown {
|
|
|
317
727
|
* the durable scheduler copy, so the ledger event stays small and the
|
|
318
728
|
* Convex append cannot wedge on an oversized/over-nested document.
|
|
319
729
|
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
730
|
+
* Results accepted by the terminal persistence contract remain durable in
|
|
731
|
+
* scheduler Postgres. Callers may set `content: "full"` only after that write
|
|
732
|
+
* succeeds; this function governs only what the ledger embeds.
|
|
322
733
|
*/
|
|
323
|
-
export function terminalRunResultForLedger(
|
|
734
|
+
export function terminalRunResultForLedger(
|
|
735
|
+
value: unknown,
|
|
736
|
+
options: { content?: 'full' } = {},
|
|
737
|
+
): unknown {
|
|
324
738
|
if (value === undefined || value === null) {
|
|
325
739
|
return value;
|
|
326
740
|
}
|
|
@@ -333,6 +747,7 @@ export function terminalRunResultForLedger(value: unknown): unknown {
|
|
|
333
747
|
return ledgerTerminalResultRefForValue(
|
|
334
748
|
candidate,
|
|
335
749
|
refReasonForIssue(issue.reason),
|
|
750
|
+
options,
|
|
336
751
|
);
|
|
337
752
|
}
|
|
338
753
|
|
|
@@ -372,6 +787,24 @@ export function assertCustomerOutputObjectWithinLimit(input: {
|
|
|
372
787
|
});
|
|
373
788
|
}
|
|
374
789
|
|
|
790
|
+
/**
|
|
791
|
+
* A play return is bounded by one serialized byte budget, never by row count.
|
|
792
|
+
* Ordinary arrays remain complete when the run succeeds.
|
|
793
|
+
*/
|
|
794
|
+
export function assertCustomerPlayReturnWithinLimit(input: {
|
|
795
|
+
value: Record<string, unknown>;
|
|
796
|
+
path?: string;
|
|
797
|
+
totalLimitBytes?: number;
|
|
798
|
+
}): number {
|
|
799
|
+
return assertJsonSized({
|
|
800
|
+
value: input.value,
|
|
801
|
+
path: input.path ?? 'play return',
|
|
802
|
+
limitBytes: input.totalLimitBytes ?? CUSTOMER_OUTPUT_TOTAL_MAX_BYTES,
|
|
803
|
+
advice:
|
|
804
|
+
'Filter rows, select only the fields you need, or return a summary before retrying.',
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
375
808
|
export function assertRuntimeReceiptOutputWithinLimit(input: {
|
|
376
809
|
output: unknown;
|
|
377
810
|
path: string;
|
|
@@ -236,10 +236,26 @@ export const RUNNER_TERMINAL_CHECKPOINT_DISPOSITION = {
|
|
|
236
236
|
export type RunnerTerminalCheckpointDisposition =
|
|
237
237
|
(typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION)[keyof typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION];
|
|
238
238
|
|
|
239
|
+
export type PlayRunOutputWarning = {
|
|
240
|
+
code: 'RUN_OUTPUT_TRUNCATED';
|
|
241
|
+
reason: 'array_preview_limit' | 'byte_limit';
|
|
242
|
+
message: string;
|
|
243
|
+
originalBytes?: number;
|
|
244
|
+
originalBytesAtLeast?: number;
|
|
245
|
+
retainedBytes?: number;
|
|
246
|
+
limitBytes?: number;
|
|
247
|
+
limitItems?: number;
|
|
248
|
+
originalItems?: number;
|
|
249
|
+
retainedItems?: number;
|
|
250
|
+
retryable: false;
|
|
251
|
+
recoverableFrom: 'runtime_sheet' | 'none';
|
|
252
|
+
};
|
|
253
|
+
|
|
239
254
|
export type PlayRunnerResult =
|
|
240
255
|
| {
|
|
241
256
|
status: 'completed';
|
|
242
257
|
output: unknown;
|
|
258
|
+
outputWarnings?: PlayRunOutputWarning[];
|
|
243
259
|
outputRowCount?: number;
|
|
244
260
|
logs: string[];
|
|
245
261
|
stats: Record<string, unknown>;
|
|
@@ -58,6 +58,10 @@ export interface RuntimeResourceGovernor {
|
|
|
58
58
|
estimatedCost?: number | null;
|
|
59
59
|
signal?: AbortSignal;
|
|
60
60
|
}): Promise<RuntimeResourceLease>;
|
|
61
|
+
acquireProviderPermit(input: {
|
|
62
|
+
toolId: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<RuntimeResourceLease>;
|
|
61
65
|
acquireSheetFlush(input?: {
|
|
62
66
|
estimatedBytes?: number | null;
|
|
63
67
|
rowCount?: number | null;
|
|
@@ -290,6 +294,13 @@ export function createRuntimeResourceGovernor(input: {
|
|
|
290
294
|
return lease;
|
|
291
295
|
},
|
|
292
296
|
|
|
297
|
+
acquireProviderPermit(providerInput) {
|
|
298
|
+
return input.executionGovernor.acquireProviderPermit(
|
|
299
|
+
providerInput.toolId,
|
|
300
|
+
{ signal: providerInput.signal },
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
|
|
293
304
|
async acquireSheetFlush(sheetInput) {
|
|
294
305
|
const startedAt = Date.now();
|
|
295
306
|
const lease = await sheetFlushAdmission.acquire(sheetInput?.signal);
|