File size: 34,883 Bytes
488d96b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
/**
 * Versionless, JSON-lines wire protocol between the Node host and the CPython subprocess. Frames
 * travel on the child's fd 3 (one JSON object per line), leaving stdout/stderr free for the
 * program's own output. Host treats every inbound frame as hostile because model code can post
 * anything through the same fd; the Python bootstrap trusts host replies.
 * @module @deepseek-ai/dsh-experimental-ptc-runtime-python/src/protocol
 */

/**
 * The framed-JSON channel's file descriptor from the child's perspective. The
 * host pins it positionally when it spawns the child (`stdio` index 3, i.e.
 * `['pipe','pipe','pipe','pipe']`), and the Python bootstrap reads the same
 * number from its own `protocol.py`. Exported as the single TS-side source of
 * truth: the host wiring uses it, and the cross-language mirror test asserts the
 * Python constant equals it, so a drift on either side breaks the boot channel
 * loudly rather than silently.
 */
export const PROTOCOL_FD = 3

/**
 * One binding namespace declaration inside a {@link BootMessage}. `global` is
 * the program-visible name the namespace is materialized under; `errorClass`,
 * when present, asks the bootstrap to mint a program-visible exception class.
 */
interface Namespace {
  global: string
  names: string[]
  errorClass?: ErrorClass
}

/**
 * A namespace's program-visible exception class: rejected calls raise its
 * instances carrying the failed member name on `memberNameProperty`.
 */
interface ErrorClass {
  name: string
  memberNameProperty: string
}

/**
 * What the host sends immediately after spawn, as the first line on fd 3. The
 * Python bootstrap reads this, applies resource limits, then waits for the
 * subsequent run frame. Separated from the run so the run message stays
 * pure model input.
 */
export interface BootMessage {
  type: 'boot'
  /** RLIMIT_CPU seconds; the Python bootstrap sets this on itself before executing model code. */
  cpuSeconds: number
  /** RLIMIT_AS bytes; caps address space so a runaway allocation fails cleanly. */
  addressSpaceBytes: number
  /** Shared byte budget for captured log text (Python-side ledger). */
  maxLogBytes: number
  /** Byte cap for the rendered completion value. */
  maxValueBytes: number
  /**
   * The namespaces to materialize inside the program (globals + names;
   * functions stay host-side). See {@link Namespace}.
   */
  namespaces: Namespace[]
}

/** Host β†’ Python: sent after `boot-ack`; carries only the model's program body. */
interface RunMessage {
  type: 'run'
  program: string
}

/** Python β†’ host: acknowledges boot completed and resource limits are in place. */
interface BootAckMessage {
  type: 'boot-ack'
}

/** Python β†’ host: one bridged binding call (`await tools.name(args)` inside the program). */
interface CallMessage {
  type: 'call'
  /** Python-issued correlation id; the host answers each id at most once and ignores duplicates. */
  id: number
  /** The namespace global the call targets. */
  global: string
  /** The function name within the namespace. */
  name: string
  /** The JSON-safe argument the model program passed. */
  args: unknown
}

/**
 * Python β†’ host: captured text, streamed eagerly so output survives a
 * mid-run termination (RLIMIT_CPU, SIGTERM/SIGKILL, host wall-timeout).
 */
interface LogMessage {
  type: 'log'
  text: string
  /**
   * Set when this frame IS the child ledger's truncation marker rather than
   * program output. The two ledgers can exhaust at different points β€” one
   * child entry larger than `maxLogBytes` sends only the marker while the host
   * ledger is still nearly empty β€” so the host cannot infer the child's state
   * from its own budget, and comparing the text against the marker string
   * would also honour a program that printed that string itself. Carrying it
   * as a field lets the host stop capturing at the same point the child did
   * and keeps exactly one marker in `logs`.
   */
  truncated?: boolean
  /**
   * Set on the frame an explicit `flush()` (or the settlement flush) pushes for
   * an UNTERMINATED line: the host holds it and appends the next log frame to
   * the same entry, so `print('a', end='', flush=True); print('b')` reads back
   * as one `'ab'` entry rather than a fake newline between two entries.
   */
  open?: boolean
}

/** The failure carried on a {@link DoneMessage}: one of three kinds plus text. */
interface DoneErrorField {
  kind: 'exception' | 'invalid-output' | 'output-limit'
  message: string
}

/**
 * Python β†’ host: the program settled. `error` carries a program exception
 * (traceback text), an `invalid-output` (completion value was not lossless
 * JSON), or an `output-limit` (serialized completion exceeded the configured
 * cap); wall/CPU budgets, aborts, and substrate death are observed host-side.
 * From the honest child `value` is present only on a clean completion that
 * produced one, and crosses as exact lossless JSON β€” never substituted or
 * truncated. A forged frame CAN carry both `value` and `error`;
 * {@link validateChildFrame} preserves both rather than guessing which to drop,
 * so a consumer MUST check `error` first and ignore `value` when it is set.
 */
interface DoneMessage {
  type: 'done'
  value?: unknown
  error?: DoneErrorField
}

/**
 * Every message the Python side sends. The member interfaces stay module-
 * private: consumers match on the union's discriminant; the host sends the
 * boot and run frames as inline literals.
 */
export type ChildToHost = BootAckMessage | CallMessage | LogMessage | DoneMessage

/** Host β†’ Python: successful answer to one {@link CallMessage}. */
interface ReplyOk {
  type: 'reply'
  id: number
  ok: true
  value: unknown
}

/** Host β†’ Python: failed answer to one {@link CallMessage}. */
interface ReplyErr {
  type: 'reply'
  id: number
  ok: false
  message: string
}

/** Host β†’ Python: the answer to one {@link CallMessage}. */
export type ReplyMessage = ReplyOk | ReplyErr

/** The required (non-optional) keys of `T`, as string literals. */
type RequiredKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K }[keyof T] & string
/** The optional keys of `T`, as string literals. */
type OptionalKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? K : never }[keyof T] & string

/**
 * Whether each key of frame `T` is a `'required'` or `'optional'` wire field.
 * Because it is `Record<keyof T, …>`, an entry MUST list every key β€” a field
 * added to the interface without a corresponding entry fails typecheck β€” and
 * `keyof T`-typed keys reject a name no frame declares. The `'required'` /
 * `'optional'` tag must match the field's actual optionality (checked by the
 * `satisfies FrameFieldRoles<…>` clause on {@link WIRE_FRAME_FIELD_ROLES}), so
 * an optionality flip is caught too. This is the exhaustive counterpart the
 * array form could not express (a subset array satisfied it silently).
 */
type FrameFieldRoles<T> = Record<RequiredKeys<T>, 'required'> & Record<OptionalKeys<T>, 'optional'>

interface WireFrameShapes {
  BootMessage: BootMessage
  Namespace: Namespace
  RunMessage: RunMessage
  BootAckMessage: BootAckMessage
  CallMessage: CallMessage
  LogMessage: LogMessage
  DoneErrorField: DoneErrorField
  DoneMessage: DoneMessage
  ErrorClass: ErrorClass
  ReplyOk: ReplyOk
  ReplyErr: ReplyErr
}

/**
 * The frames carried on a message union: everything the host and child send as
 * a top-level frame (`ChildToHost`, the two reply variants, and the host→child
 * boot/run frames). The nested shapes `Namespace`, `ErrorClass`, and
 * `DoneErrorField` are fields of other frames, not frames themselves, so they
 * are excluded here and covered only by the roles `satisfies` and the mirror e2e.
 */
type MessageFrames = ChildToHost | ReplyMessage | BootMessage | RunMessage
/** The roster's value types minus the three nested (non-frame) shapes. */
type RosterMessageFrames = Exclude<WireFrameShapes[keyof WireFrameShapes], Namespace | ErrorClass | DoneErrorField>

/**
 * Compile-time proof that {@link WireFrameShapes}'s message-frame entries are
 * EXACTLY the frames on the message unions β€” checked BOTH directions. Forward
 * (`MessageFrames extends RosterMessageFrames`) catches a frame added to a union
 * without a roster entry; reverse (`RosterMessageFrames extends MessageFrames`)
 * catches a frame removed from a union while the roster still lists it (e.g.
 * dropping `ReplyErr` from `ReplyMessage`). Either divergence makes an alias
 * `false`, failing the assignment below. Type-only; the `const`s emit nothing
 * meaningful at runtime.
 */
type UnionSubsetOfRoster = [MessageFrames] extends [RosterMessageFrames] ? true : false
type RosterSubsetOfUnion = [RosterMessageFrames] extends [MessageFrames] ? true : false
const _unionSubsetOfRoster: UnionSubsetOfRoster = true
const _rosterSubsetOfUnion: RosterSubsetOfUnion = true
void _unionSubsetOfRoster
void _rosterSubsetOfUnion

/**
 * Each frame's wire fields tagged by required/optional, keyed by field name so
 * the mapping is exhaustive over the frame interface (see {@link FrameFieldRoles})
 * across the whole {@link WireFrameShapes} roster. Bound to the interfaces by
 * `satisfies` below; {@link WIRE_FRAME_FIELDS} projects it to sorted
 * required/optional arrays for the cross-language mirror comparison. `global` is
 * the JSON key {@link CallMessage} and {@link Namespace} send (a reserved word
 * the Python side carries via a functional `TypedDict`).
 */
const WIRE_FRAME_FIELD_ROLES = {
  BootMessage: { type: 'required', cpuSeconds: 'required', addressSpaceBytes: 'required', maxLogBytes: 'required', maxValueBytes: 'required', namespaces: 'required' },
  Namespace: { global: 'required', names: 'required', errorClass: 'optional' },
  RunMessage: { type: 'required', program: 'required' },
  BootAckMessage: { type: 'required' },
  CallMessage: { type: 'required', id: 'required', global: 'required', name: 'required', args: 'required' },
  LogMessage: { type: 'required', text: 'required', truncated: 'optional', open: 'optional' },
  DoneErrorField: { kind: 'required', message: 'required' },
  DoneMessage: { type: 'required', value: 'optional', error: 'optional' },
  ErrorClass: { name: 'required', memberNameProperty: 'required' },
  ReplyOk: { type: 'required', id: 'required', ok: 'required', value: 'required' },
  ReplyErr: { type: 'required', id: 'required', ok: 'required', message: 'required' },
} as const satisfies { [K in keyof WireFrameShapes]: FrameFieldRoles<WireFrameShapes[K]> }

/**
 * The wire field names of each frame, split into sorted required and optional
 * key arrays β€” the shape the cross-language mirror test compares against
 * `py/protocol.py`'s `TypedDict` `__required_keys__`/`__optional_keys__`.
 * Projected from {@link WIRE_FRAME_FIELD_ROLES}, so it inherits that mapping's
 * exhaustive, optionality-checked binding to the frame interfaces: a TS-side
 * field add, remove, rename, or optionality flip fails typecheck at the roles
 * map, and a Python-side divergence fails the mirror test at runtime.
 */
export const WIRE_FRAME_FIELDS =
  Object.fromEntries(
    Object.entries(WIRE_FRAME_FIELD_ROLES).map(([frame, roles]) => {
      const required = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'required').sort()
      const optional = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'optional').sort()
      return [frame, { required, optional }]
    }),
  ) as Record<keyof typeof WIRE_FRAME_FIELD_ROLES, { required: string[]; optional: string[] }>


/**
 * The in-band marker text announcing that log capture stopped at the byte
 * budget. Shared wire vocabulary: the Python-side LogBuffer emits it when ITS
 * ledger exhausts, and the host emits identical text when its own ledger drops
 * a frame first (forged fd-3 traffic, stray stdout bytes) β€” a truncated run
 * reads the same however the cap was hit.
 * @param maxBytes - the configured `maxLogBytes` the marker names.
 * @returns the marker line.
 */
export function logTruncationMarker(maxBytes: number): string {
  return `[dsh-ptc-runtime-python] log capture truncated at ${maxBytes} bytes`
}

/**
 * Serialize one JSON-parse-produced value without recursion. `JSON.stringify`
 * recurses per nesting level and throws `RangeError` a few thousand levels
 * deep, but the seam's `PtcJsonValue` has no depth limit β€” an honest deep
 * completion or binding resolution below the byte budget must cross intact
 * (the worker backend's wire is equally stack-safe). Callers must pass a value
 * produced by `JSON.parse` (or equally JSON-plain): only `null`, finite
 * numbers, booleans, strings, dense arrays, and plain objects β€” this encoder
 * validates nothing. Output matches compact `JSON.stringify` byte for byte
 * EXCEPT on an integral double beyond the safe range, where {@link scalarJson}
 * emits the exact integer's BigInt digits rather than `JSON.stringify`'s rounded
 * spelling (`1152921504606846976`, not `...847000`) so the seam's lossless-JSON
 * promise holds across the wire.
 * @param value - a JSON-plain value (e.g. straight from `JSON.parse`).
 * @returns the compact JSON encoding.
 */
export function encodeJsonPlain(value: unknown): string {
  // The task stack holds every member of the currently open containers β€” O(width)
  // β€” but the encoded OUTPUT is itself O(total bytes) and the stack holds only
  // references, so the walk's auxiliary state is same-order as its result; the
  // metering walks (checkDoneValue/hasNonLosslessNumber) are the ones that must
  // stay O(depth), since they can reject a wide payload without producing any
  // output. Exempted by that same-order argument.
  type Task = { text: string } | { value: unknown }
  const chunks: string[] = []
  const tasks: Task[] = [{ value }]
  for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
    if ('text' in task) {
      chunks.push(task.text)
      continue
    }
    const current = task.value
    if (typeof current === 'string') {
      chunks.push(JSON.stringify(current))
    } else if (Array.isArray(current)) {
      chunks.push('[')
      tasks.push({ text: ']' })
      for (let index = current.length - 1; index >= 0; index--) {
        if (index < current.length - 1) tasks.push({ text: ',' })
        tasks.push({ value: current[index] })
      }
    } else if (typeof current === 'object' && current !== null) {
      const record = current as Record<string, unknown>
      chunks.push('{')
      tasks.push({ text: '}' })
      const keys = Object.keys(record)
      for (let index = keys.length - 1; index >= 0; index--) {
        const key = keys[index] as string
        if (index < keys.length - 1) tasks.push({ text: ',' })
        tasks.push({ value: record[key] })
        tasks.push({ text: `${JSON.stringify(key)}:` })
      }
    } else {
      chunks.push(scalarJson(current))
    }
  }
  return chunks.join('')
}

/**
 * One scalar (null, boolean, finite number) as JSON text. A beyond-safe-range
 * integral double needs BigInt digits: `String(2 ** 60)` emits the ROUNDED
 * `...847000` form, and echoing that to the child would silently change the
 * integer the seam promised to carry losslessly β€” `BigInt(2 ** 60)` prints the
 * exact `...846976` the double actually holds.
 * @param current - a JSON-plain scalar (JSON.parse emits nothing else).
 * @returns its JSON encoding.
 */
function scalarJson(current: unknown): string {
  if (typeof current === 'number' && Number.isInteger(current) && !Number.isSafeInteger(current)) {
    return BigInt(current).toString()
  }
  return String(current)
}

/**
 * Exact UTF-8 byte length of one string's compact JSON form (quotes + escapes),
 * computed by a single non-allocating scan that stops the instant the running
 * total exceeds `maxBytes`. Used instead of `Buffer.byteLength(JSON.stringify(s))`
 * so a control-heavy forged string β€” whose escaped copy expands up to ~6x β€” is
 * rejected BEFORE that copy is materialized: `JSON.stringify` would allocate the
 * full escaped form first, the very hundreds-of-MB spike the metered traversal
 * exists to avoid. Mirrors `JSON.stringify`'s escaping byte-for-byte: `"` and
 * `\` and the five short C0 escapes cost 2, other C0 controls `\uXXXX` cost 6, a
 * valid surrogate pair is one astral code point emitted as raw 4-byte UTF-8, a
 * LONE surrogate becomes `\uXXXX` at 6, and any other code point costs its raw
 * UTF-8 width.
 * @param text - the string to meter.
 * @param maxBytes - largest serialized size the caller can still admit.
 * @returns the exact serialized byte length, or `undefined` once it exceeds `maxBytes`.
 */
function jsonStringBytesUpTo(text: string, maxBytes: number): number | undefined {
  let bytes = 2 // the two quotes
  if (bytes > maxBytes) return undefined
  for (let index = 0; index < text.length; index++) {
    const code = text.charCodeAt(index)
    if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
      bytes += 2 // `\"` `\\` `\b` `\t` `\n` `\f` `\r`
    } else if (code < 0x20) {
      bytes += 6 // other C0 controls: `\uXXXX`
    } else if (code < 0x80) {
      bytes += 1
    } else if (code < 0x800) {
      bytes += 2
    } else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length) {
      const next = text.charCodeAt(index + 1)
      if (next >= 0xdc00 && next <= 0xdfff) {
        bytes += 4 // valid high+low pair: one astral code point, raw 4-byte UTF-8
        index++
      } else {
        bytes += 6 // lone high surrogate: `\uXXXX`
      }
    } else if (code >= 0xd800 && code <= 0xdfff) {
      bytes += 6 // lone surrogate (unpaired high at end, or any low): `\uXXXX`
    } else {
      bytes += 3 // other BMP code point
    }
    if (bytes > maxBytes) return undefined
  }
  return bytes
}

/**
 * Meter a `JSON.parse`-produced done value's compact-JSON byte length AND its
 * number losslessness in one traversal, stopping the instant `maxBytes` is
 * crossed. This bounds the INCREMENTAL allocation the check itself would add on
 * top of the already-parsed value β€” the enqueued children; strings and keys are
 * metered by {@link jsonStringBytesUpTo} without allocating an escaped copy β€”
 * not the parse that produced `value`.
 * That upstream width is bounded separately, by the host-side cap on inbound
 * fd-3 frame size before `JSON.parse` runs (owned by the runtime that reads the
 * channel), so `value` cannot be arbitrarily large when it reaches here. The
 * budget is the `maxValueBytes` the boot frame carries β€” a required wire field
 * with no default at this layer. The traversal rejects over-budget BEFORE
 * materializing a string's escaped form or enqueuing an array's/object's
 * children, so a forgery within that frame cap cannot force those secondary
 * allocations. Object key COUNTING is
 * unavoidably O(keys) β€” JS has no lazy own-key iterator, and the parse already
 * built the key set β€” but the check still refuses the per-entry work before the
 * enqueue loop. A non-lossless number (non-finite, negative zero) is caught only
 * when the value fits the budget β€” an over-budget value is rejected regardless,
 * so the distinction is moot. Same JSON-plain precondition and traversal shape
 * as {@link encodeJsonPlain}; a number's byte length is measured through
 * {@link scalarJson} (matching the encoder, so a beyond-safe-range integer
 * meters its exact BigInt digits, not `JSON.stringify`'s rounded spelling) and
 * a string's/key's through {@link jsonStringBytesUpTo} (the exact escaped size,
 * scanned without allocating the escaped copy).
 * @param value - a JSON-plain value (e.g. straight from `JSON.parse`).
 * @param maxBytes - the completion-value budget in bytes.
 * @returns `{ ok: true, bytes }` with the exact serialized size, or
 * `{ ok: false, reason }` β€” `over-budget` once the size exceeds `maxBytes`,
 * `non-lossless` on a non-finite or negative-zero number.
 */
export function checkDoneValue(value: unknown, maxBytes: number): { ok: true; bytes: number } | { ok: false; reason: 'over-budget' | 'non-lossless' } {
  let bytes = 0
  // A non-lossless number is recorded, not returned on sight: over-budget must
  // win regardless of where in the value each violation sits, so the whole
  // metering finishes first. Otherwise `["<huge>", 1e400]` and `[1e400,
  // "<huge>"]` β€” the same over-budget value in two member orders β€” would
  // classify differently (non-lossless vs over-budget), and the JSDoc promises
  // an over-budget value is rejected as over-budget regardless.
  let nonLossless = false
  // One cursor per OPEN container (a values iterator for the root and arrays,
  // an entries iterator for objects), mirroring hasNonLosslessNumber and the
  // child's _check_done_value: a wide completion near the frame cap would
  // otherwise copy every member's reference onto an explicit work stack β€”
  // O(width) β€” OOMing the host after the parse already succeeded. The byte
  // budget still bounds the walk: each member is metered as its cursor yields
  // it, and the width lower-bound checks below bail an over-budget container
  // before the cursor descends.
  const cursors: Cursor[] = [{ kind: 'values', iter: [value].values() }]
  while (cursors.length > 0) {
    // The loop condition guarantees a top cursor.
    const cursor = cursors.at(-1) as Cursor
    const step = cursor.iter.next()
    if (step.done === true) {
      cursors.pop()
      continue
    }
    let current: unknown
    if (cursor.kind === 'entries') {
      // Meter the key's escaped form without allocating it (same reason as the
      // string branch), then add the colon separator, before the value's own
      // bytes are counted.
      const [key, member] = step.value as readonly [string, unknown]
      const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes)
      if (keyBytes === undefined) return { ok: false, reason: 'over-budget' }
      bytes += keyBytes + 1
      current = member
    } else {
      current = step.value
    }
    if (typeof current === 'number') {
      // Flag a non-lossless number but keep counting its encoded bytes: a value
      // that is BOTH non-lossless and over-budget must classify as over-budget
      // (the loop's byte check below wins), so the byte count cannot skip the
      // offending number. `scalarJson` gives the same spelling a legit scalar
      // would meter.
      if (!Number.isFinite(current) || Object.is(current, -0)) nonLossless = true
      bytes += Buffer.byteLength(scalarJson(current), 'utf8')
    } else if (typeof current === 'string') {
      // Meter the escaped form WITHOUT allocating it: jsonStringBytesUpTo scans
      // and bails the instant the running cost crosses the remaining budget, so
      // a control-heavy forgery (escaped copy up to ~6x) never materializes that
      // copy the way `JSON.stringify` would.
      const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes)
      if (stringBytes === undefined) return { ok: false, reason: 'over-budget' }
      bytes += stringBytes
    } else if (Array.isArray(current)) {
      // Brackets plus one comma per gap; elements add themselves. Reject
      // BEFORE the cursor descends: every element serializes to at least one
      // byte, so a forged flat array far above the budget fails here without
      // the cursor yielding any of them. (The array itself is already
      // materialized by the upstream parse; this only bounds the extra walk.)
      bytes += 2 + (current.length > 1 ? current.length - 1 : 0)
      if (bytes + current.length > maxBytes) return { ok: false, reason: 'over-budget' }
      cursors.push({ kind: 'values', iter: (current as unknown[]).values() })
    } else if (typeof current === 'object' && current !== null) {
      const record = current as Record<string, unknown>
      // Count own keys with for...in + hasOwn. This IS O(keys) β€” JS has no lazy
      // own-key iterator and the parse already built the key set β€” so the count
      // cannot be sublinear; what the bound below buys is refusing the per-entry
      // work (key escaping, value enqueue) before it runs. Each entry costs at
      // least a quoted key (>= 2 bytes) + colon + >= 1-byte value.
      let count = 0
      for (const key in record) if (Object.hasOwn(record, key)) count += 1
      bytes += 2 + (count > 1 ? count - 1 : 0)
      if (bytes + count * 4 > maxBytes) return { ok: false, reason: 'over-budget' }
      cursors.push({ kind: 'entries', iter: ownEntries(record) })
    } else {
      bytes += Buffer.byteLength(scalarJson(current), 'utf8')
    }
    if (bytes > maxBytes) return { ok: false, reason: 'over-budget' }
  }
  // The whole value fit the budget; a recorded number violation is the verdict.
  if (nonLossless) return { ok: false, reason: 'non-lossless' }
  return { ok: true, bytes }
}

/**
 * Whether a raw JSON line contains an integer token that would lose precision
 * as a JavaScript number. `JSON.parse` silently rounds such a token
 * (`9007199254740993` becomes `...992`) BEFORE any validation can see it, so
 * the check must read the source text; a beyond-safe-range token whose double
 * parse round-trips exactly (`2**53`, `2**60`) is lossless and passes. The scan walks the line skipping string literals (a digit run
 * inside a string is data, not a number token) and tests every number token
 * in plain integer form β€” no fraction or exponent, which parse as doubles by
 * intent. A reviver cannot do this job: the reviver walk recurses per nesting
 * level and would reintroduce the depth limit `encodeJsonPlain` removes.
 * @param line - the raw UTF-8 text of one JSON-lines frame.
 * @returns true when an unsafe integer token is present outside strings.
 */
export function hasUnsafeIntegerToken(line: string): boolean {
  for (let index = 0; index < line.length; index++) {
    const char = line[index]
    if (char === '"') {
      // Skip the string literal, honoring backslash escapes.
      for (index++; index < line.length; index++) {
        if (line[index] === '\\') index++
        else if (line[index] === '"') break
      }
      continue
    }
    if (char === '-' || (char !== undefined && char >= '0' && char <= '9')) {
      let end = index + 1
      while (end < line.length) {
        const c = line[end] as string
        if ((c >= '0' && c <= '9') || c === '.' || c === 'e' || c === 'E' || c === '+' || c === '-') end++
        else break
      }
      const token = line.slice(index, end)
      // Beyond the safe range an integer token is still lossless IFF the
      // double parse round-trips exactly (2**53 does; 2**53+1 rounds) β€” the
      // canonical boundary accepts every JS-double-exact value, so only a
      // genuinely rounding token marks the frame as forged.
      if (/^-?\d+$/.test(token)) {
        const parsed = Number(token)
        // A token that parses to Infinity is trivially lossy; a finite
        // beyond-safe-range one is lossy only when the BigInt round-trip
        // disagrees.
        if (!Number.isFinite(parsed)) return true
        if (!Number.isSafeInteger(parsed) && BigInt(token) !== BigInt(parsed)) return true
      }
      index = end - 1
    }
  }
  return false
}

/**
 * One open container in checkDoneValue's cursor walk: a values iterator (the
 * root and arrays) or an entries iterator (objects, so each key's escaped
 * bytes can be metered when the entry is reached). A cursor bounds the walk's
 * auxiliary state to O(depth), not O(width).
 */
type Cursor =
  | { kind: 'values'; iter: Iterator<unknown> }
  | { kind: 'entries'; iter: Iterator<readonly [string, unknown]> }

/**
 * Lazily yield one plain object's own enumerable [key, value] entries. The
 * key escapes are metered when {@link checkDoneValue}'s cursor walk reaches
 * each entry, so a wide object never materializes a member list: each entry
 * is produced straight off the already-parsed record, and the escaped key
 * bytes are counted without building the escaped string.
 * @param record - a JSON-parse-produced object.
 * @yields each own enumerable [key, value] pair, in key order.
 */
function* ownEntries(record: Record<string, unknown>): Generator<readonly [string, unknown]> {
  for (const key in record) {
    if (Object.hasOwn(record, key)) yield [key, record[key]]
  }
}

/**
 * Lazily yield one plain object's own enumerable property values. A generator
 * (not `Object.values`/`Object.entries`) because {@link hasNonLosslessNumber}
 * walks breadth it cannot bound: those helpers copy the whole VALUE (or
 * key/value pair) list into a fresh array up front, so a wide object would cost
 * that second full-breadth allocation before a single value is examined. The
 * `for...in` here does not make the walk sublinear β€” V8 still materializes the
 * key-name enumeration when the loop starts β€” but it avoids the extra value
 * array, yielding each value straight off the already-parsed object.
 * @param record - a JSON-parse-produced object.
 * @yields each own enumerable property value, in key order.
 */
function* ownValues(record: object): Generator {
  for (const key in record) {
    if (Object.hasOwn(record, key)) yield (record as Record<string, unknown>)[key]
  }
}

/**
 * Whether a JSON.parse-produced value contains a number outside lossless
 * JSON: non-finite (`1e400` parses to `Infinity`) or negative zero (`-0.0`
 * parses to JS `-0`, whose sign bit a re-serialization drops). The honest
 * child's validator rejects these before sending, so a frame carrying one is
 * forged.
 *
 * Runs on `call.args`, which β€” unlike a completion value β€” has NO seam byte
 * cap, so there is no budget to reject a wide payload against the way
 * {@link checkDoneValue} does. The traversal therefore holds ONE cursor per
 * NESTING LEVEL (an array or {@link ownValues} iterator) instead of one entry
 * per member: a forged flat `args` at the top of the host's inbound frame-size
 * cap would
 * otherwise push tens of millions of stack entries β€” and `Object.values` would
 * copy each object's full breadth β€” allocating hundreds of megabytes beyond
 * what `JSON.parse` already holds. Iterative either way, so a deep frame
 * cannot overflow the host stack.
 * @param value - a JSON-parse-produced value from an fd-3 frame.
 * @returns true when any contained number is non-finite or negative zero.
 */
export function hasNonLosslessNumber(value: unknown): boolean {
  const cursors: Iterator<unknown>[] = [[value].values()]
  while (cursors.length > 0) {
    // The loop condition guarantees a top cursor.
    const cursor = cursors.at(-1) as Iterator<unknown>
    const step = cursor.next()
    if (step.done === true) {
      cursors.pop()
      continue
    }
    const current = step.value
    if (typeof current === 'number') {
      if (!Number.isFinite(current) || Object.is(current, -0)) return true
    } else if (Array.isArray(current)) {
      cursors.push((current as unknown[]).values())
    } else if (typeof current === 'object' && current !== null) {
      cursors.push(ownValues(current))
    }
  }
  return false
}

/**
 * Runtime shape gate for inbound fd-3 traffic. Model code has full access to
 * fd 3 and can post anything β€” `null`, primitives, poisoned fields β€” so the
 * compile-time union means nothing here: every field is validated and REBUILT
 * before the host reads it (forged extras never ride along; a non-number id
 * can never be echoed into a reply). Junk returns `undefined` and is dropped
 * so a throw in the host's `message` handler cannot crash the host process.
 * @param raw - one JSON-parsed frame from fd 3.
 * @returns the rebuilt frame, or `undefined` to drop it silently.
 */
export function validateChildFrame(raw: unknown): ChildToHost | undefined {
  if (typeof raw !== 'object' || raw === null) return undefined
  const m = raw as Record<string, unknown>
  switch (m.type) {
    case 'boot-ack':
      return { type: 'boot-ack' }
    case 'log':
      if (typeof m.text !== 'string') return undefined
      // Rebuilt, not passed through: a forged `truncated` of any other type
      // would reach the host as a truthy value and silence capture for the
      // rest of the run. Only the literal `true` counts; `open` likewise.
      return {
        type: 'log',
        text: m.text,
        ...m.truncated === true ? { truncated: true } : {},
        ...m.open === true ? { open: true } : {},
      }
    case 'call': {
      // The id must be a finite number: it is echoed verbatim into the reply
      // frame, and a forged `1e400` id (Infinity after JSON.parse) would make
      // the reply unencodable as strict JSON. Negative zero is rejected too:
      // it passes `Number.isFinite`, but the reply re-serializes it as `0`
      // (`JSON.stringify({id:-0})` is `{"id":0}`), colliding with a real call
      // whose id is `0` β€” the honest child never issues `-0`.
      if (typeof m.id !== 'number' || !Number.isFinite(m.id) || Object.is(m.id, -0) || typeof m.global !== 'string' || typeof m.name !== 'string') return undefined
      // A forged frame can omit `args` entirely; rebuilding it as `undefined`
      // would invoke the binding with a non-JSON value, bypassing the
      // lossless-JSON argument boundary. Any PRESENT value is JSON-plain by
      // construction (the frame came from JSON.parse), so presence is the
      // whole check.
      if (!Object.hasOwn(m, 'args')) return undefined
      // JSON.parse yields Infinity for 1e400 and preserves -0; both are
      // outside lossless JSON, and the honest child never sends them.
      if (hasNonLosslessNumber(m.args)) return undefined
      return { type: 'call', id: m.id, global: m.global, name: m.name, args: m.args }
    }
    case 'done': {
      // The value passes through untouched here: scanning it for non-lossless
      // numbers would push every member of a wide forged payload before any
      // byte cap runs. The done handler's bounded `checkDoneValue` folds the
      // losslessness check into the metered traversal, rejecting over-budget
      // before it enqueues children.
      const err = m.error
      if (err === undefined) {
        return m.value === undefined ? { type: 'done' } : { type: 'done', value: m.value }
      }
      if (typeof err !== 'object' || err === null) return undefined
      const { kind, message } = err as Record<string, unknown>
      if (typeof message !== 'string') return undefined
      if (kind !== 'exception' && kind !== 'invalid-output' && kind !== 'output-limit') return undefined
      return m.value === undefined
        ? { type: 'done', error: { kind, message } }
        : { type: 'done', value: m.value, error: { kind, message } }
    }
    default:
      return undefined
  }
}