/** * The durable terminal envelope carries the complete 5 MiB authored return * plus bounded scheduler-owned metadata (progress, warnings, and a log tail). * It must not make a return that passed the customer contract fail later. */ export const TERMINAL_RUN_RESULT_MAX_BYTES = 6 * 1024 * 1024; /** * Convex hard-caps a single document at 1 MiB and rejects nesting deeper than * 16 levels. A run-ledger `run.completed` / `run.failed` event embeds its * `result` inside one `playRunEvents` document, so the ledger-bound result must * stay comfortably under that ceiling (the event also carries seq/type/ * timestamps and the surrounding doc envelope). Keep well below 1 MiB so no * single event can wedge the append mutation with `Value is too large (>1MiB)`. * Historically a failed run embedded its full `PlayRunnerResult` — including a * `checkpoint` whose `completedToolBatches[*].toolResponse.raw` held megabytes * of provider payload — which is durable in scheduler Postgres and never read * back from Convex, yet deterministically wedged the append. */ // The terminal projection is persisted twice on `playRuns` (top-level result // and runSnapshot.result), in addition to the event document. Keep one copy at // 256 KiB so ordinary snapshots retain headroom under Convex's 1 MiB document // ceiling. The runtime route has a reference-only fallback for an unusually // large pre-existing snapshot that still exceeds the aggregate document cap. export const LEDGER_TERMINAL_RESULT_MAX_BYTES = 256 * 1024; export const LEDGER_TERMINAL_RESULT_MAX_DEPTH = 10; export const CONVEX_LEDGER_ARRAY_MAX_ITEMS = 8_192; export const CONVEX_LEDGER_OBJECT_MAX_FIELDS = 1_024; export const CONVEX_LEDGER_FIELD_NAME_MAX_LENGTH = 1_024; /** Customer-visible values and complete authored returns share one 5 MiB cap. */ export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 5 * 1024 * 1024; export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 5 * 1024 * 1024; /** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */ export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024; /** One completion request may contain multiple receipts up to 32 MiB total. */ export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024; /** * The Fly receipt gateway accepts one runner-terminal control request up to * this size. This is a transport ceiling, not a customer-output contract: * settled customer returns remain capped at 5 MiB and the 6 MiB durable * terminal envelope; a suspended checkpoint is retained separately because it * is required to resume execution. */ export const RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES = 16 * 1024 * 1024; /** * Receipt commands are measured before the runtime client adds its request * envelope. Keep a full 4 MiB below the gateway body ceiling so one legal * 10 MiB receipt fits while compatible fat receipts split before transport. */ export const RUNTIME_RECEIPT_GATEWAY_BATCH_MAX_BYTES = 12 * 1024 * 1024; /** * Aim below the hard gateway ceiling when coalescing receipts. One legal large * receipt may exceed this target and is still sent alone; the target only * prevents several multi-megabyte results from monopolizing the serialized * gateway writer behind one request. */ export const RUNTIME_RECEIPT_WRITER_TARGET_BATCH_BYTES = 4 * 1024 * 1024; /** * The terminal `result` line is the crash-recovery record in Daytona stdout. * Once that line is written, retries may still emit runner-owned diagnostics. * Keep their aggregate UTF-8 footprint below this allowance so a gateway-size * suspended checkpoint remains inside the crash pusher's bounded tail read. */ export const RUNNER_POST_TERMINAL_DIAGNOSTIC_MAX_BYTES = 64 * 1024; export class OutputTooLargeError extends Error { readonly code = 'OUTPUT_TOO_LARGE'; readonly path: string; readonly bytes: number; readonly limitBytes: number; readonly measuredExactly: boolean; constructor(input: { path: string; bytes: number; limitBytes: number; advice: string; measuredExactly?: boolean; }) { const sizeDescription = input.measuredExactly === false ? 'at least ' : ''; super( `OUTPUT_TOO_LARGE: ${input.path} is ${sizeDescription}${formatBytes(input.bytes)}, above the ${formatBytes(input.limitBytes)} limit. ` + input.advice, ); this.name = 'OutputTooLargeError'; this.path = input.path; this.bytes = input.bytes; this.limitBytes = input.limitBytes; this.measuredExactly = input.measuredExactly !== false; } } export function jsonByteLength(value: unknown): number { const serialized = JSON.stringify(value); if (serialized === undefined) return 0; return utf8ByteLength(serialized); } type BoundedJsonByteLength = { bytes: number; exceeded: boolean; exact: boolean; }; const JSON_SIZE_LIMIT_REACHED = Symbol('JSON_SIZE_LIMIT_REACHED'); function utf8ByteLength(value: string): number { let bytes = 0; for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code <= 0x7f) { bytes += 1; } else if (code <= 0x7ff) { bytes += 2; } else if (code >= 0xd800 && code <= 0xdbff) { const next = value.charCodeAt(index + 1); if (next >= 0xdc00 && next <= 0xdfff) { bytes += 4; index += 1; } else { bytes += 3; } } else { bytes += 3; } } return bytes; } function jsonStringByteLength(value: string): number { let bytes = 2; for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code === 0x22 || code === 0x5c) { bytes += 2; } else if (code <= 0x1f) { bytes += code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6; } else if (code <= 0x7f) { bytes += 1; } else if (code <= 0x7ff) { bytes += 2; } else if (code >= 0xd800 && code <= 0xdbff) { const next = value.charCodeAt(index + 1); if (next >= 0xdc00 && next <= 0xdfff) { bytes += 4; index += 1; } else { bytes += 6; } } else if (code >= 0xdc00 && code <= 0xdfff) { bytes += 6; } else { bytes += 3; } } return bytes; } /** * Measure JSON only up to a caller-owned ceiling. The replacer accounts for a * lower bound before JSON.stringify builds its result and aborts as soon as * that bound crosses the limit. Oversized strings are scanned, never copied. */ export function jsonByteLengthUpTo( value: unknown, limitBytes: number, ): BoundedJsonByteLength { let observedBytes = 0; const add = (bytes: number) => { observedBytes += bytes; if (observedBytes > limitBytes) throw JSON_SIZE_LIMIT_REACHED; }; try { const serialized = JSON.stringify( value, function (this: unknown, key, entry: unknown) { const arrayParent = Array.isArray(this); const omittedObjectEntry = !arrayParent && (entry === undefined || typeof entry === 'function' || typeof entry === 'symbol'); if (key && !arrayParent && !omittedObjectEntry) { add(jsonStringByteLength(key) + 1); } if (typeof entry === 'string') { add(jsonStringByteLength(entry)); } else if (typeof entry === 'number') { add(Number.isFinite(entry) ? String(entry).length : 4); } else if (typeof entry === 'boolean') { add(entry ? 4 : 5); } else if (entry === null) { add(4); } else if ( arrayParent && (entry === undefined || typeof entry === 'function' || typeof entry === 'symbol') ) { add(4); } else if (entry && typeof entry === 'object') { // At least one opening delimiter. This makes deeply repeated empty // containers advance the bound even before punctuation is counted. add(1); } return entry; }, ); if (serialized === undefined) { return { bytes: 0, exceeded: false, exact: true }; } const bytes = utf8ByteLength(serialized); return { bytes, exceeded: bytes > limitBytes, exact: true }; } catch (error) { if (error === JSON_SIZE_LIMIT_REACHED) { return { bytes: observedBytes, exceeded: true, exact: false }; } throw error; } } function formatBytes(bytes: number): string { if (bytes >= 1024 * 1024) { return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`; } if (bytes >= 1024) { return `${(bytes / 1024).toFixed(1)} KiB`; } return `${bytes} B`; } function assertJsonSized(input: { value: unknown; path: string; limitBytes: number; advice: string; }): number { let measurement: BoundedJsonByteLength; try { measurement = jsonByteLengthUpTo(input.value, input.limitBytes); } catch (error) { throw new Error( `${input.path} cannot be serialized as JSON: ${ error instanceof Error ? error.message : String(error) }`, ); } if (measurement.exceeded) { throw new OutputTooLargeError({ path: input.path, bytes: measurement.bytes, limitBytes: input.limitBytes, advice: input.advice, measuredExactly: measurement.exact, }); } return measurement.bytes; } export function valueWithinJsonByteLimit(value: unknown, limitBytes: number) { if (value === undefined || value === null) { return value; } return jsonByteLengthUpTo(value, limitBytes).exceeded ? undefined : value; } /** Shared terminal-result contract for every scheduler backend. */ export function persistableTerminalRunResult(value: unknown): unknown { return valueWithinJsonByteLimit(value, TERMINAL_RUN_RESULT_MAX_BYTES); } /** A completed run must never commit without its complete terminal value. */ export function assertTerminalRunResultWithinLimit(value: unknown): unknown { assertJsonSized({ value, path: 'terminal run result', limitBytes: TERMINAL_RUN_RESULT_MAX_BYTES, advice: 'The complete terminal result cannot be persisted. Keep authored output within its documented limits and keep transport metadata bounded.', }); return value; } /** * Replay-only keys a terminal `PlayRunnerResult` carries for the scheduler * (Postgres) data plane. These are NEVER read back from the Convex run ledger: * resume rehydrates the checkpoint/suspension from scheduler Postgres * (`work_runs.checkpoint_json` / `suspension_json`), and customer reads use the * run's `output`. Embedding them in a `playRunEvents` document only bloats the * write and can exceed Convex's 1 MiB / 16-level document limits. */ const LEDGER_STRIPPED_TERMINAL_RESULT_KEYS = [ 'checkpoint', 'suspension', ] as const; /** A tiny reference descriptor left in place of a dropped over-limit result. */ export type LedgerTerminalResultRef = { __kind: 'deepline.ledger_terminal_result_ref.v1'; /** Where the full terminal result is durably stored. */ store: 'scheduler_postgres'; /** Column on the scheduler `work_runs` row that holds the full result. */ key: 'terminal_result_json'; /** Serialized byte size of the result that was elided from the ledger. */ bytes?: number; reason: LedgerTerminalResultRefReason; /** The live Convex projection was intentionally replaced, not lost. */ projection?: 'out_of_line'; /** New refs point only at a value that was actually persisted in full. */ content?: 'full'; /** Bounded customer-safe explanation; contains no payload paths or keys. */ warning?: string; /** * Compact, Convex-safe run-list projection captured while the terminal * result is already in memory. List/live polling reads this instead of * fetching the complete scheduler result. */ preview?: BoundedRunListOutputPreview; }; export const LEDGER_TERMINAL_RESULT_STORED_WARNING = '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.'; export const LEDGER_TERMINAL_RESULT_OMITTED_WARNING = '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.'; export type LedgerTerminalResultRefReason = | 'terminal_result_exceeds_ledger_limit' | 'terminal_result_exceeds_ledger_depth' | 'terminal_result_has_invalid_field_name' | 'terminal_result_array_too_long' | 'terminal_result_object_too_wide' | 'terminal_result_not_json_serializable' | 'terminal_result_rejected_by_convex'; export type ConvexLedgerPayloadIssue = { reason: | 'size_limit' | 'depth_limit' | 'invalid_field_name' | 'array_too_long' | 'object_too_wide' | 'not_json_serializable'; bytes?: number; }; function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function isConvexFieldName(value: string): boolean { if ( value.length === 0 || value.length > CONVEX_LEDGER_FIELD_NAME_MAX_LENGTH || value.startsWith('$') ) { return false; } for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code < 32 || code >= 127) return false; } return true; } function isSafeRunListPreviewFieldName(value: string): boolean { return ( isConvexFieldName(value) && value !== '__proto__' && value !== 'constructor' && value !== 'prototype' ); } /** * The dashboard's historical run list is a polling surface. Keep its output * projection tiny even when the authored return is the full 5 MiB allowed by * the customer contract. These limits deliberately mirror the API response * budget, but live here so the durable Convex summary can be written without * importing a server-only route helper. */ export const RUN_LIST_OUTPUT_PREVIEW_LIMITS = { maxFields: 40, // The list endpoint returns multiple historical runs in one polling page. // Reserve enough room for run state and legacy rows by keeping each preview // at 4 KiB; the complete authored result remains separately readable up to // the 5 MiB customer-output ceiling. maxBytes: 4_000, maxDepth: 4, maxStringLength: 300, maxArrayItems: 5, maxObjectFields: 40, } as const; export type BoundedRunListOutputPreview = { output: Record; truncated: boolean; }; function compactRunListPreviewString(value: string): { value: string; truncated: boolean; } { const compact = value.replace(/\s+/g, ' ').trim(); if (compact.length <= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxStringLength) { return { value: compact, truncated: compact !== value }; } return { value: `${compact.slice(0, RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxStringLength)}...`, truncated: true, }; } /** * Produce a small projection that satisfies Convex's field-name/depth rules. * Recursion stops at a small, caller-owned depth so an unsafe result can never * make its own preview projection exhaust the stack. */ function compactRunListPreviewValue( value: unknown, depth: number, ): { value: unknown; truncated: boolean } { if ( value == null || typeof value === 'number' || typeof value === 'boolean' ) { return { value, truncated: false }; } if (typeof value === 'string') { return compactRunListPreviewString(value); } if (depth >= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxDepth) { return { value: Array.isArray(value) ? '[Array]' : '[Object]', truncated: true, }; } if (Array.isArray(value)) { let truncated = value.length > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxArrayItems; const items: unknown[] = []; for (const item of value.slice( 0, RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxArrayItems, )) { const compacted = compactRunListPreviewValue(item, depth + 1); truncated ||= compacted.truncated; items.push(compacted.value); } return { value: items, truncated }; } if (!isPlainObject(value)) { return { value: String(value), truncated: true }; } const output: Record = Object.create(null) as Record< string, unknown >; let truncated = false; const entries = Object.entries(value); for (const [key, child] of entries.slice( 0, RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxObjectFields, )) { if (!isSafeRunListPreviewFieldName(key)) { truncated = true; continue; } const compacted = compactRunListPreviewValue(child, depth + 1); truncated ||= compacted.truncated; output[key] = compacted.value; } return { value: output, truncated: truncated || entries.length > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxObjectFields, }; } function runListPreviewFieldBytes( key: string, value: unknown, hasPreviousField: boolean, ): number { try { return utf8ByteLength( `${hasPreviousField ? ',' : ''}${JSON.stringify(key)}:${JSON.stringify(value)}`, ); } catch { return Number.POSITIVE_INFINITY; } } /** * Build the durable run-list preview while the terminal result is already in * memory. It never reads a full result from Postgres, and it does not copy * invalid Convex field names into the summary projection. */ export function buildBoundedRunListOutputPreview( value: Record, ): BoundedRunListOutputPreview { const output: Record = Object.create(null) as Record< string, unknown >; let outputBytes = 2; let outputFields = 0; let truncated = false; for (const [key, child] of Object.entries(value)) { if (outputFields >= RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxFields) { truncated = true; break; } if (!isSafeRunListPreviewFieldName(key)) { truncated = true; continue; } const compacted = compactRunListPreviewValue(child, 0); truncated ||= compacted.truncated; let previewValue = compacted.value; let fieldBytes = runListPreviewFieldBytes( key, previewValue, outputFields > 0, ); if (outputBytes + fieldBytes > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxBytes) { truncated = true; previewValue = '[Preview omitted; open the run for complete output]'; fieldBytes = runListPreviewFieldBytes( key, previewValue, outputFields > 0, ); if (outputBytes + fieldBytes > RUN_LIST_OUTPUT_PREVIEW_LIMITS.maxBytes) { break; } } output[key] = previewValue; outputBytes += fieldBytes; outputFields += 1; } return { output, truncated }; } /** * Read the preview that is safe to carry in Convex run summaries. Older refs * did not persist a preview; mark those as truncated so callers can link to * the explicit full-run read without falling back to an unbounded hydration. */ export function runListOutputPreviewForLedger( value: unknown, ): BoundedRunListOutputPreview | null { if (isLedgerTerminalResultRef(value)) { return value.preview ?? { output: {}, truncated: true }; } return isPlainObject(value) ? buildBoundedRunListOutputPreview(value) : null; } /** * Validate the part of Convex's value contract that can permanently poison a * durable run-ledger append. The walk is iterative and stops at the depth * boundary, so adversarial nesting cannot overflow the JS stack while being * classified. It intentionally returns no path or customer field name. */ export function inspectConvexLedgerPayload( value: unknown, ): ConvexLedgerPayloadIssue | null { const stack: Array<{ value: unknown; depth: number }> = [ { value, depth: value !== null && typeof value === 'object' ? 1 : 0, }, ]; while (stack.length > 0) { const current = stack.pop()!; if (current.depth > LEDGER_TERMINAL_RESULT_MAX_DEPTH) { return { reason: 'depth_limit' }; } if (Array.isArray(current.value)) { if (current.value.length > CONVEX_LEDGER_ARRAY_MAX_ITEMS) { return { reason: 'array_too_long' }; } for (const item of current.value) { stack.push({ value: item, depth: item !== null && typeof item === 'object' ? current.depth + 1 : current.depth, }); } continue; } if (!current.value || typeof current.value !== 'object') continue; const entries = Object.entries(current.value as Record); if (entries.length > CONVEX_LEDGER_OBJECT_MAX_FIELDS) { return { reason: 'object_too_wide' }; } for (const [key, entry] of entries) { if (!isConvexFieldName(key)) { return { reason: 'invalid_field_name' }; } stack.push({ value: entry, depth: entry !== null && typeof entry === 'object' ? current.depth + 1 : current.depth, }); } } try { const measurement = jsonByteLengthUpTo( value, LEDGER_TERMINAL_RESULT_MAX_BYTES, ); return measurement.exceeded ? { reason: 'size_limit', bytes: measurement.bytes } : null; } catch { return { reason: 'not_json_serializable' }; } } export function isLedgerTerminalResultRef( value: unknown, ): value is LedgerTerminalResultRef { return ( isPlainObject(value) && value.__kind === 'deepline.ledger_terminal_result_ref.v1' && value.store === 'scheduler_postgres' && value.key === 'terminal_result_json' ); } export function stripReplayOnlyTerminalResult(value: unknown): unknown { if (!isPlainObject(value) || isLedgerTerminalResultRef(value)) return value; const stripped: Record = {}; for (const [key, entry] of Object.entries(value)) { if ( (LEDGER_STRIPPED_TERMINAL_RESULT_KEYS as readonly string[]).includes(key) ) { continue; } stripped[key] = entry; } return stripped; } function refReasonForIssue( issue: ConvexLedgerPayloadIssue['reason'], ): LedgerTerminalResultRefReason { switch (issue) { case 'size_limit': return 'terminal_result_exceeds_ledger_limit'; case 'depth_limit': return 'terminal_result_exceeds_ledger_depth'; case 'invalid_field_name': return 'terminal_result_has_invalid_field_name'; case 'array_too_long': return 'terminal_result_array_too_long'; case 'object_too_wide': return 'terminal_result_object_too_wide'; case 'not_json_serializable': return 'terminal_result_not_json_serializable'; } } function ledgerTerminalResultRef( reason: LedgerTerminalResultRefReason, options: { content?: 'full'; bytes?: number; preview?: BoundedRunListOutputPreview; } = {}, ): LedgerTerminalResultRef { return { __kind: 'deepline.ledger_terminal_result_ref.v1', store: 'scheduler_postgres', key: 'terminal_result_json', ...(options.bytes !== undefined ? { bytes: options.bytes } : {}), reason, projection: 'out_of_line', ...(options.content ? { content: options.content } : {}), ...(options.preview ? { preview: options.preview } : {}), warning: options.content ? LEDGER_TERMINAL_RESULT_STORED_WARNING : LEDGER_TERMINAL_RESULT_OMITTED_WARNING, }; } export function ledgerTerminalResultRefForValue( value: unknown, reason: LedgerTerminalResultRefReason, options: { content?: 'full' } = {}, ): LedgerTerminalResultRef { let bytes: number | undefined; try { bytes = jsonByteLengthUpTo(value, LEDGER_TERMINAL_RESULT_MAX_BYTES).bytes; } catch { // The bounded reason is the useful diagnostic. Never serialize the // customer payload again merely to populate reference metadata. } let preview: BoundedRunListOutputPreview | undefined; if (options.content && isPlainObject(value)) { try { preview = buildBoundedRunListOutputPreview(value); } catch { // The terminal reference is the durable fallback. A hostile/non-JSON // value must not turn optional list-preview derivation into a terminal // persistence failure. } } return ledgerTerminalResultRef(reason, { ...options, bytes, preview }); } /** Optional derived summaries must never block the terminal lifecycle event. */ export function ledgerSummaryForLedger(value: unknown): unknown { if (value === undefined || value === null) return value; return inspectConvexLedgerPayload(value) ? undefined : value; } /** * Shape a terminal run result for the Convex run ledger. Control-plane only: * * 1. Strip replay-only keys ({@link LEDGER_STRIPPED_TERMINAL_RESULT_KEYS}) from * the top-level result object — the scheduler Postgres data plane owns them. * 2. If what remains still exceeds {@link LEDGER_TERMINAL_RESULT_MAX_BYTES}, * replace the whole result with a {@link LedgerTerminalResultRef} pointing at * the durable scheduler copy, so the ledger event stays small and the * Convex append cannot wedge on an oversized/over-nested document. * * Results accepted by the terminal persistence contract remain durable in * scheduler Postgres. Callers may set `content: "full"` only after that write * succeeds; this function governs only what the ledger embeds. */ export function terminalRunResultForLedger( value: unknown, options: { content?: 'full' } = {}, ): unknown { if (value === undefined || value === null) { return value; } if (isLedgerTerminalResultRef(value)) return value; const candidate = stripReplayOnlyTerminalResult(value); const issue = inspectConvexLedgerPayload(candidate); if (!issue) { return candidate; } return ledgerTerminalResultRefForValue( candidate, refReasonForIssue(issue.reason), options, ); } export function assertCustomerOutputValueWithinLimit(input: { value: unknown; path: string; limitBytes?: number; }): number { return assertJsonSized({ value: input.value, path: input.path, limitBytes: input.limitBytes ?? CUSTOMER_OUTPUT_VALUE_MAX_BYTES, advice: 'Deepline can process large provider/cache payloads, but customer-visible outputs must stay bounded. Extract the fields you need before returning or saving this value.', }); } export function assertCustomerOutputObjectWithinLimit(input: { value: Record; path: string; totalLimitBytes?: number; valueLimitBytes?: number; }): number { for (const [key, value] of Object.entries(input.value)) { assertCustomerOutputValueWithinLimit({ value, path: `${input.path}.${key}`, limitBytes: input.valueLimitBytes, }); } return assertJsonSized({ value: input.value, path: input.path, limitBytes: input.totalLimitBytes ?? CUSTOMER_OUTPUT_TOTAL_MAX_BYTES, advice: 'This looks like too much data for one customer-visible output object. Split the work into rows, or parse the provider response and save only the columns you actually need.', }); } /** * A play return is bounded by one serialized byte budget, never by row count. * Ordinary arrays remain complete when the run succeeds. */ export function assertCustomerPlayReturnWithinLimit(input: { value: Record; path?: string; totalLimitBytes?: number; }): number { return assertJsonSized({ value: input.value, path: input.path ?? 'play return', limitBytes: input.totalLimitBytes ?? CUSTOMER_OUTPUT_TOTAL_MAX_BYTES, advice: 'Filter rows, select only the fields you need, or return a summary before retrying.', }); } export function assertRuntimeReceiptOutputWithinLimit(input: { output: unknown; path: string; limitBytes?: number; }): number { return assertJsonSized({ value: input.output, path: input.path, limitBytes: input.limitBytes ?? RUNTIME_RECEIPT_OUTPUT_MAX_BYTES, advice: 'This receipt result is too large for durable call caching. This usually means raw search results, HTML, or a large list is being cached as one cell. Parse it before returning it from the tool boundary.', }); }