@ian-pascoe/pi-codemode 0.1.0 → 0.3.0

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.
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from "node:buffer";
2
+ import { CODEMODE_CONSOLE_METHODS, type CodeModeConsoleEntry } from "./codemode-console-output.ts";
2
3
 
3
4
  /** Maximum UTF-8 bytes in one CodeMode worker protocol line, excluding its newline. */
4
5
  export const CODEMODE_WORKER_MESSAGE_LIMIT_BYTES = 8 * 1024 * 1024;
@@ -76,6 +77,7 @@ export type CodeModeWorkerResponse =
76
77
  readonly sessionId: string;
77
78
  readonly cellId: string;
78
79
  readonly resultJson?: string;
80
+ readonly console?: readonly CodeModeConsoleEntry[];
79
81
  }
80
82
  | {
81
83
  readonly version: 1;
@@ -83,6 +85,7 @@ export type CodeModeWorkerResponse =
83
85
  readonly sessionId: string;
84
86
  readonly cellId: string;
85
87
  readonly error: { readonly code: CodeModeWorkerCellErrorCode; readonly message: string };
88
+ readonly console?: readonly CodeModeConsoleEntry[];
86
89
  }
87
90
  | {
88
91
  readonly version: 1;
@@ -132,19 +135,61 @@ function hasString(values: readonly string[], candidate: string): boolean {
132
135
  function hasExactKeys(value: CodeModeProtocolObject, expected: readonly string[]): boolean {
133
136
  const keys = objectKeys(value);
134
137
  if (keys.length !== expected.length) return false;
135
- for (let expectedIndex = 0; expectedIndex < expected.length; expectedIndex += 1) {
136
- const expectedKey = expected[expectedIndex];
137
- if (expectedKey === undefined) return false;
138
- let found = false;
139
- for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
140
- if (keys[keyIndex] === expectedKey) {
141
- found = true;
142
- break;
138
+ return expected.every((expectedKey) => expectedKey !== undefined && keys.includes(expectedKey));
139
+ }
140
+
141
+ function hasDuplicateJsonObjectKeys(message: string): boolean {
142
+ const objectKeysByDepth: (string[] | undefined)[] = [];
143
+ for (let index = 0; index < message.length; index += 1) {
144
+ const character = message[index];
145
+ if (character === "{") {
146
+ objectKeysByDepth.push([]);
147
+ continue;
148
+ }
149
+ if (character === "[") {
150
+ objectKeysByDepth.push(undefined);
151
+ continue;
152
+ }
153
+ if (character === "}" || character === "]") {
154
+ objectKeysByDepth.pop();
155
+ continue;
156
+ }
157
+ if (character !== '"') continue;
158
+
159
+ let endIndex = index + 1;
160
+ for (; endIndex < message.length; endIndex += 1) {
161
+ if (message[endIndex] === "\\") {
162
+ endIndex += 1;
163
+ continue;
143
164
  }
165
+ if (message[endIndex] === '"') break;
144
166
  }
145
- if (!found) return false;
167
+ if (endIndex >= message.length) return false;
168
+ let nextIndex = endIndex + 1;
169
+ while (
170
+ message[nextIndex] === " " ||
171
+ message[nextIndex] === "\n" ||
172
+ message[nextIndex] === "\r" ||
173
+ message[nextIndex] === "\t"
174
+ ) {
175
+ nextIndex += 1;
176
+ }
177
+ const keys = objectKeysByDepth[objectKeysByDepth.length - 1];
178
+ if (message[nextIndex] === ":" && keys !== undefined) {
179
+ try {
180
+ // SAFETY: This slice is one syntactically bounded JSON string token; the string refinement below rejects every other JSON value.
181
+ const key = jsonParse(message.slice(index, endIndex + 1)) as CodeModeProtocolValue;
182
+ if (isString(key)) {
183
+ if (hasString(keys, key)) return true;
184
+ keys.push(key);
185
+ }
186
+ } catch {
187
+ return false;
188
+ }
189
+ }
190
+ index = endIndex;
146
191
  }
147
- return true;
192
+ return false;
148
193
  }
149
194
 
150
195
  function parseProtocolJson(
@@ -158,6 +203,9 @@ function parseProtocolJson(
158
203
  if (Buffer.byteLength(message, "utf8") > CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) {
159
204
  return { ok: false, message: `CodeMode worker ${subject} exceeds 8 MiB` };
160
205
  }
206
+ if (hasDuplicateJsonObjectKeys(message)) {
207
+ return { ok: false, message: `CodeMode worker ${subject} contains duplicate object keys` };
208
+ }
161
209
  try {
162
210
  // SAFETY: Successful JSON.parse output is exactly the recursive JSON representation modeled by CodeModeProtocolValue.
163
211
  return { ok: true, value: jsonParse(message) as CodeModeProtocolValue };
@@ -308,6 +356,30 @@ function parseWorkerError(
308
356
  return { code: value.code, message: value.message };
309
357
  }
310
358
 
359
+ function parseConsoleOutput(
360
+ value: CodeModeProtocolSlot,
361
+ ): readonly CodeModeConsoleEntry[] | undefined {
362
+ if (!arrayIsArray(value) || value.length === 0) return undefined;
363
+ const entries: CodeModeConsoleEntry[] = [];
364
+ for (const candidate of value) {
365
+ if (
366
+ !isRecord(candidate) ||
367
+ !hasExactKeys(candidate, ["method", "text"]) ||
368
+ !isString(candidate.method) ||
369
+ !hasString(CODEMODE_CONSOLE_METHODS, candidate.method) ||
370
+ !isString(candidate.text)
371
+ ) {
372
+ return undefined;
373
+ }
374
+ entries.push({
375
+ // SAFETY: The literal-membership check above refines the protocol string to a supported Console method.
376
+ method: candidate.method as CodeModeConsoleEntry["method"],
377
+ text: candidate.text,
378
+ });
379
+ }
380
+ return entries;
381
+ }
382
+
311
383
  function parseToolBatchResponse(
312
384
  decoded: CodeModeProtocolObject,
313
385
  ): CodeModeWorkerResponse | undefined {
@@ -401,43 +473,58 @@ export function parseCodeModeWorkerResponse(
401
473
  }
402
474
  if (
403
475
  decoded.type === "cell-result" &&
404
- hasExactKeys(
405
- decoded,
406
- decoded.resultJson === undefined
407
- ? ["cellId", "sessionId", "type", "version"]
408
- : ["cellId", "resultJson", "sessionId", "type", "version"],
409
- ) &&
410
476
  isNonEmptyString(decoded.cellId) &&
411
477
  (decoded.resultJson === undefined || isString(decoded.resultJson))
412
478
  ) {
479
+ const expectedKeys = ["cellId", "sessionId", "type", "version"];
480
+ if (decoded.resultJson !== undefined) expectedKeys.push("resultJson");
481
+ if (decoded.console !== undefined) expectedKeys.push("console");
482
+ if (!hasExactKeys(decoded, expectedKeys)) {
483
+ return { ok: false, message: "CodeMode worker response has an invalid protocol shape" };
484
+ }
485
+ const consoleEntries =
486
+ decoded.console === undefined ? undefined : parseConsoleOutput(decoded.console);
487
+ if (decoded.console !== undefined && consoleEntries === undefined) {
488
+ return { ok: false, message: "CodeMode worker response has an invalid protocol shape" };
489
+ }
413
490
  const value = {
414
491
  version: CODEMODE_WORKER_PROTOCOL_VERSION,
415
492
  type: "cell-result",
416
493
  sessionId: decoded.sessionId,
417
494
  cellId: decoded.cellId,
418
495
  } as const;
419
- return decoded.resultJson === undefined
420
- ? { ok: true, value }
421
- : { ok: true, value: { ...value, resultJson: decoded.resultJson } };
496
+ const resultValue =
497
+ decoded.resultJson === undefined ? value : { ...value, resultJson: decoded.resultJson };
498
+ return consoleEntries === undefined
499
+ ? { ok: true, value: resultValue }
500
+ : { ok: true, value: { ...resultValue, console: consoleEntries } };
422
501
  }
423
- if (
424
- decoded.type === "cell-error" &&
425
- hasExactKeys(decoded, ["cellId", "error", "sessionId", "type", "version"]) &&
426
- isNonEmptyString(decoded.cellId)
427
- ) {
502
+ if (decoded.type === "cell-error" && isNonEmptyString(decoded.cellId)) {
503
+ const expectedKeys = ["cellId", "error", "sessionId", "type", "version"];
504
+ if (decoded.console !== undefined) expectedKeys.push("console");
505
+ if (!hasExactKeys(decoded, expectedKeys)) {
506
+ return { ok: false, message: "CodeMode worker response has an invalid protocol shape" };
507
+ }
428
508
  const error = parseWorkerError(decoded.error);
429
- if (error !== undefined && ["script", "serialization", "runtime"].includes(error.code)) {
509
+ const consoleEntries =
510
+ decoded.console === undefined ? undefined : parseConsoleOutput(decoded.console);
511
+ if (
512
+ error !== undefined &&
513
+ ["script", "serialization", "runtime"].includes(error.code) &&
514
+ (decoded.console === undefined || consoleEntries !== undefined)
515
+ ) {
430
516
  // SAFETY: The literal-membership check above refines the protocol string to the closed worker error code union.
431
517
  const code = error.code as CodeModeWorkerCellErrorCode;
518
+ const value = {
519
+ version: CODEMODE_WORKER_PROTOCOL_VERSION,
520
+ type: "cell-error",
521
+ sessionId: decoded.sessionId,
522
+ cellId: decoded.cellId,
523
+ error: { code, message: error.message },
524
+ } as const;
432
525
  return {
433
526
  ok: true,
434
- value: {
435
- version: CODEMODE_WORKER_PROTOCOL_VERSION,
436
- type: "cell-error",
437
- sessionId: decoded.sessionId,
438
- cellId: decoded.cellId,
439
- error: { code, message: error.message },
440
- },
527
+ value: consoleEntries === undefined ? value : { ...value, console: consoleEntries },
441
528
  };
442
529
  }
443
530
  }
@@ -456,7 +543,28 @@ export function serializeCodeModeWorkerRequest(
456
543
 
457
544
  /** Serializes one worker response, replacing oversized Cell output with a bounded error. */
458
545
  export function serializeCodeModeWorkerResponse(response: CodeModeWorkerResponse): string {
459
- const message = jsonStringify(response);
546
+ let message: string;
547
+ if (response.type === "cell-result" && response.console?.length === 0) {
548
+ const responseBase = {
549
+ version: response.version,
550
+ type: response.type,
551
+ sessionId: response.sessionId,
552
+ cellId: response.cellId,
553
+ } as const;
554
+ message = jsonStringify(
555
+ response.resultJson === undefined
556
+ ? responseBase
557
+ : { ...responseBase, resultJson: response.resultJson },
558
+ );
559
+ } else if (response.type === "cell-error" && response.console?.length === 0) {
560
+ message = jsonStringify({
561
+ version: response.version,
562
+ type: response.type,
563
+ sessionId: response.sessionId,
564
+ cellId: response.cellId,
565
+ error: response.error,
566
+ });
567
+ } else message = jsonStringify(response);
460
568
  if (Buffer.byteLength(message, "utf8") <= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) return message;
461
569
  if (
462
570
  response.type === "cell-result" ||
@@ -1,3 +1,4 @@
1
+ import { CODEMODE_CONSOLE_METHODS, type CodeModeConsoleEntry } from "./codemode-console-output.ts";
1
2
  import {
2
3
  CODEMODE_WORKER_MESSAGE_LIMIT_BYTES,
3
4
  parseCodeModeWorkerRequest,
@@ -9,13 +10,27 @@ import {
9
10
  } from "./codemode-worker-protocol.ts";
10
11
 
11
12
  const CODEMODE_WORKER_READ_BUFFER_BYTES = 64 * 1024;
13
+ const CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES = 512;
12
14
 
13
15
  type DenoByteReader = { read(buffer: Uint8Array): Promise<number | null> };
14
16
  type DenoByteWriter = { write(buffer: Uint8Array): Promise<number> };
17
+ type DenoInspectOptions = {
18
+ readonly colors: false;
19
+ readonly getters: false;
20
+ readonly customInspect: false;
21
+ };
15
22
  type CodeModeDenoNamespace = {
16
23
  readonly args: readonly string[];
17
24
  readonly stdin: DenoByteReader;
18
25
  readonly stdout: DenoByteWriter;
26
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Deno.inspect is the captured hostile-value formatter; coordinator tests cover getters, coercion hooks, custom inspectors, and Proxies.
27
+ readonly inspect: (value: unknown, options: DenoInspectOptions) => string;
28
+ readonly internal: symbol;
29
+ readonly [key: symbol]:
30
+ | {
31
+ readonly inspectArgs: (args: readonly unknown[], options: DenoInspectOptions) => string;
32
+ }
33
+ | undefined;
19
34
  readonly version: {
20
35
  readonly deno: string;
21
36
  readonly v8: string;
@@ -26,6 +41,11 @@ type CodeModeDenoNamespace = {
26
41
  declare const Deno: CodeModeDenoNamespace;
27
42
 
28
43
  const denoProcess = Deno;
44
+ const denoInspect = denoProcess.inspect;
45
+ const denoInternal = denoProcess[denoProcess.internal];
46
+ if (denoInternal === undefined)
47
+ throw new Error("Pi CodeMode: Deno Console formatter is unavailable");
48
+ const denoInspectArgs = denoInternal.inspectArgs;
29
49
  const arrayIsArray = Array.isArray;
30
50
  const arrayPrototype = Array.prototype;
31
51
  const blobConstructor = Blob;
@@ -43,6 +63,11 @@ const numberFrom = Number;
43
63
  const numberIsFinite = Number.isFinite;
44
64
  const numberIsSafeInteger = Number.isSafeInteger;
45
65
  const objectFreeze = Object.freeze;
66
+ const SAFE_DENO_INSPECT_OPTIONS = objectFreeze({
67
+ colors: false,
68
+ getters: false,
69
+ customInspect: false,
70
+ } as const);
46
71
  const objectPrototype = Object.prototype;
47
72
  const ownKeys = Reflect.ownKeys;
48
73
  const queueRuntimeMicrotask = queueMicrotask.bind(globalThis);
@@ -159,6 +184,9 @@ type ActiveWorkerCell = {
159
184
  readonly sessionId: string;
160
185
  readonly cellId: string;
161
186
  readonly pendingCalls: PendingGuestToolCall[];
187
+ readonly consoleEntries: CodeModeConsoleEntry[];
188
+ consoleBytes: number;
189
+ consoleOverflow: boolean;
162
190
  batchSequence: number;
163
191
  callSequence: number;
164
192
  batchScheduled: boolean;
@@ -206,6 +234,113 @@ function isReservedNotebookBindingName(name: string): boolean {
206
234
  return false;
207
235
  }
208
236
 
237
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; captured Deno.inspect disables getters and custom inspection, while coordinator hostile-value tests cover coercion hooks and Proxies.
238
+ function inspectGuestConsoleValue(value: unknown): string {
239
+ try {
240
+ return denoInspect(value, SAFE_DENO_INSPECT_OPTIONS);
241
+ } catch {
242
+ return "[Uninspectable value]";
243
+ }
244
+ }
245
+
246
+ function formatGuestConsoleArguments(args: readonly unknown[]): string | undefined {
247
+ if (args.length === 0) return "";
248
+ const first = args[0];
249
+ const maximumTextLength =
250
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES - CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES;
251
+ if (isGuestString(first) && first.length >= maximumTextLength) return undefined;
252
+ if (args.length === 1 && isGuestString(first)) return first;
253
+ if (isGuestString(first) && !first.includes("%")) {
254
+ let text = first;
255
+ for (let index = 1; index < args.length; index += 1) {
256
+ const value = args[index];
257
+ const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
258
+ if (text.length + rendered.length + 1 >= maximumTextLength) return undefined;
259
+ text += ` ${rendered}`;
260
+ }
261
+ return text;
262
+ }
263
+ if (!isGuestString(first)) {
264
+ let text = "";
265
+ for (let index = 0; index < args.length; index += 1) {
266
+ const value = args[index];
267
+ const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
268
+ const separatorLength = index === 0 ? 0 : 1;
269
+ if (text.length + rendered.length + separatorLength >= maximumTextLength) return undefined;
270
+ if (separatorLength > 0) text += " ";
271
+ text += rendered;
272
+ }
273
+ return text;
274
+ }
275
+
276
+ const safeArgs: unknown[] = [first];
277
+ for (let index = 1; index < args.length; index += 1) safeArgs[index] = args[index];
278
+ let format = "";
279
+ let argumentIndex = 1;
280
+ for (let index = 0; index < first.length; index += 1) {
281
+ const character = first[index];
282
+ if (character !== "%" || index + 1 >= first.length) {
283
+ format += character;
284
+ continue;
285
+ }
286
+ const token = first[index + 1];
287
+ if (token === "%") {
288
+ format += "%%";
289
+ index += 1;
290
+ continue;
291
+ }
292
+ if (
293
+ token !== "s" &&
294
+ token !== "d" &&
295
+ token !== "i" &&
296
+ token !== "f" &&
297
+ token !== "j" &&
298
+ token !== "o" &&
299
+ token !== "O" &&
300
+ token !== "c"
301
+ ) {
302
+ format += `%${token}`;
303
+ index += 1;
304
+ continue;
305
+ }
306
+ const value = safeArgs[argumentIndex];
307
+ if (value !== undefined || argumentIndex < safeArgs.length) {
308
+ if (isGuestReference(value)) {
309
+ const inspected = inspectGuestConsoleValue(value);
310
+ if (inspected.length >= maximumTextLength) return undefined;
311
+ safeArgs[argumentIndex] = inspected;
312
+ format += token === "c" ? "%c" : "%s";
313
+ } else {
314
+ format += `%${token}`;
315
+ }
316
+ argumentIndex += 1;
317
+ } else {
318
+ format += `%${token}`;
319
+ }
320
+ index += 1;
321
+ }
322
+ safeArgs[0] = format;
323
+ for (let index = argumentIndex; index < safeArgs.length; index += 1) {
324
+ const value = safeArgs[index];
325
+ if (isGuestReference(value)) {
326
+ const inspected = inspectGuestConsoleValue(value);
327
+ if (inspected.length >= maximumTextLength) return undefined;
328
+ safeArgs[index] = inspected;
329
+ }
330
+ }
331
+ let minimumFormattedBytes = 0;
332
+ for (const value of safeArgs) {
333
+ if (isGuestString(value)) minimumFormattedBytes += value.length;
334
+ if (
335
+ minimumFormattedBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
336
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
337
+ ) {
338
+ return undefined;
339
+ }
340
+ }
341
+ return denoInspectArgs(safeArgs, SAFE_DENO_INSPECT_OPTIONS);
342
+ }
343
+
209
344
  function assertNotebookBindingNameAvailable(name: string): void {
210
345
  if (isReservedNotebookBindingName(name)) {
211
346
  throw new typeErrorConstructor(`CodeMode Notebook Binding '${name}' is reserved`);
@@ -398,6 +533,35 @@ function utf8ByteLength(value: string): number {
398
533
  return encodeUtf8(value).byteLength;
399
534
  }
400
535
 
536
+ function utf8JsonStringByteLength(value: string): number {
537
+ let bytes = 2;
538
+ for (let index = 0; index < value.length; index += 1) {
539
+ const codeUnit = value.charCodeAt(index);
540
+ if (codeUnit === 0x22 || codeUnit === 0x5c) bytes += 2;
541
+ else if (codeUnit <= 0x1f) {
542
+ bytes +=
543
+ codeUnit === 0x08 ||
544
+ codeUnit === 0x09 ||
545
+ codeUnit === 0x0a ||
546
+ codeUnit === 0x0c ||
547
+ codeUnit === 0x0d
548
+ ? 2
549
+ : 6;
550
+ } else if (codeUnit <= 0x7f) bytes += 1;
551
+ else if (codeUnit <= 0x7ff) bytes += 2;
552
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
553
+ const lowSurrogate = value.charCodeAt(index + 1);
554
+ if (lowSurrogate >= 0xdc00 && lowSurrogate <= 0xdfff) {
555
+ bytes += 4;
556
+ index += 1;
557
+ } else bytes += 6;
558
+ } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) bytes += 6;
559
+ else bytes += 3;
560
+ if (bytes >= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) return bytes;
561
+ }
562
+ return bytes;
563
+ }
564
+
401
565
  // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This is arbitrary guest ingress; descriptor-only traversal avoids invoking accessors, coercion, and guest-mutated methods. Coordinator hostile JSON tests cover the boundary.
402
566
  function serializeGuestJson(value: unknown, allowUndefined: boolean): string | undefined {
403
567
  const seen: object[] = [];
@@ -715,6 +879,50 @@ defineProperty(globalThis, "tools", {
715
879
  value: tools,
716
880
  writable: false,
717
881
  });
882
+ const guestConsole = createObject(null);
883
+ for (const method of CODEMODE_CONSOLE_METHODS) {
884
+ defineProperty(guestConsole, method, {
885
+ configurable: false,
886
+ enumerable: true,
887
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; formatGuestConsoleArguments safely inspects them before capture.
888
+ value: (...args: unknown[]): undefined => {
889
+ const cell = activeCell;
890
+ if (cell !== undefined && !cell.consoleOverflow) {
891
+ const text = formatGuestConsoleArguments(args);
892
+ if (text === undefined) {
893
+ cell.consoleEntries.length = 0;
894
+ cell.consoleOverflow = true;
895
+ return undefined;
896
+ }
897
+ const entryBytes = 19 + utf8JsonStringByteLength(method) + utf8JsonStringByteLength(text);
898
+ const nextConsoleBytes =
899
+ cell.consoleBytes + (cell.consoleEntries.length === 0 ? 0 : 1) + entryBytes;
900
+ if (
901
+ nextConsoleBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
902
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
903
+ ) {
904
+ cell.consoleEntries.length = 0;
905
+ cell.consoleOverflow = true;
906
+ return undefined;
907
+ }
908
+ cell.consoleEntries[cell.consoleEntries.length] = {
909
+ method,
910
+ text,
911
+ };
912
+ cell.consoleBytes = nextConsoleBytes;
913
+ }
914
+ return undefined;
915
+ },
916
+ writable: false,
917
+ });
918
+ }
919
+ objectFreeze(guestConsole);
920
+ defineProperty(globalThis, "console", {
921
+ configurable: false,
922
+ enumerable: false,
923
+ value: guestConsole,
924
+ writable: false,
925
+ });
718
926
 
719
927
  function disableGuestGlobal(name: string, value?: Readonly<typeof safeDenoIdentity>): void {
720
928
  const descriptor = getOwnPropertyDescriptor(globalThis, name);
@@ -770,7 +978,6 @@ const safeDenoIdentity = objectFreeze({
770
978
  disableGuestGlobal("Deno", safeDenoIdentity);
771
979
  for (const unsafeGlobal of [
772
980
  "process",
773
- "console",
774
981
  "alert",
775
982
  "confirm",
776
983
  "prompt",
@@ -889,42 +1096,97 @@ function scheduleCellFinish(cell: ActiveWorkerCell): void {
889
1096
  return;
890
1097
  }
891
1098
  activeCell = undefined;
1099
+ const consoleEntries = cell.consoleEntries.length === 0 ? undefined : [...cell.consoleEntries];
892
1100
  let response: CodeModeWorkerResponse;
893
- if (cell.mainFailed) {
894
- const error = describeGuestError(cell.mainError);
895
- const serializationFailure =
896
- isGuestReference(cell.mainError) &&
897
- (hasSerializationErrorInstance(cell.mainError) ||
898
- internalToolErrorCode(cell.mainError) === "serialization");
1101
+ if (cell.consoleOverflow) {
899
1102
  response = {
900
1103
  version: 1,
901
1104
  type: "cell-error",
902
1105
  sessionId: cell.sessionId,
903
1106
  cellId: cell.cellId,
904
- error: {
905
- code: serializationFailure ? "serialization" : "script",
906
- message: renderGuestError(error),
907
- },
1107
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
908
1108
  };
909
- } else {
910
- try {
911
- const resultJson = serializeGuestJson(cell.mainResult, true);
1109
+ } else if (cell.mainFailed) {
1110
+ const error = describeGuestError(cell.mainError);
1111
+ const serializationFailure =
1112
+ isGuestReference(cell.mainError) &&
1113
+ (hasSerializationErrorInstance(cell.mainError) ||
1114
+ internalToolErrorCode(cell.mainError) === "serialization");
1115
+ const message = renderGuestError(error);
1116
+ if (
1117
+ consoleEntries !== undefined &&
1118
+ cell.consoleBytes +
1119
+ utf8JsonStringByteLength(message) +
1120
+ CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
1121
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
1122
+ ) {
1123
+ response = {
1124
+ version: 1,
1125
+ type: "cell-error",
1126
+ sessionId: cell.sessionId,
1127
+ cellId: cell.cellId,
1128
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
1129
+ };
1130
+ } else {
912
1131
  const responseBase = {
913
1132
  version: 1,
914
- type: "cell-result",
1133
+ type: "cell-error",
915
1134
  sessionId: cell.sessionId,
916
1135
  cellId: cell.cellId,
1136
+ error: {
1137
+ code: serializationFailure ? "serialization" : "script",
1138
+ message,
1139
+ },
917
1140
  } as const;
918
- response = resultJson === undefined ? responseBase : { ...responseBase, resultJson };
1141
+ response =
1142
+ consoleEntries === undefined
1143
+ ? responseBase
1144
+ : { ...responseBase, console: consoleEntries };
1145
+ }
1146
+ } else {
1147
+ try {
1148
+ const resultJson = serializeGuestJson(cell.mainResult, true);
1149
+ if (
1150
+ consoleEntries !== undefined &&
1151
+ cell.consoleBytes +
1152
+ (resultJson === undefined ? 0 : utf8JsonStringByteLength(resultJson)) +
1153
+ CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
1154
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
1155
+ ) {
1156
+ response = {
1157
+ version: 1,
1158
+ type: "cell-error",
1159
+ sessionId: cell.sessionId,
1160
+ cellId: cell.cellId,
1161
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
1162
+ };
1163
+ } else {
1164
+ const responseBase = {
1165
+ version: 1,
1166
+ type: "cell-result",
1167
+ sessionId: cell.sessionId,
1168
+ cellId: cell.cellId,
1169
+ } as const;
1170
+ const resultResponse =
1171
+ resultJson === undefined ? responseBase : { ...responseBase, resultJson };
1172
+ response =
1173
+ consoleEntries === undefined
1174
+ ? resultResponse
1175
+ : { ...resultResponse, console: consoleEntries };
1176
+ }
919
1177
  } catch (cause) {
920
1178
  const error = describeGuestError(cause);
921
- response = {
1179
+ const responseBase = {
922
1180
  version: 1,
923
1181
  type: "cell-error",
924
1182
  sessionId: cell.sessionId,
925
1183
  cellId: cell.cellId,
926
1184
  error: { code: "serialization", message: renderGuestError(error) },
927
- };
1185
+ } as const;
1186
+ response =
1187
+ consoleEntries === undefined
1188
+ ? responseBase
1189
+ : { ...responseBase, console: consoleEntries };
928
1190
  }
929
1191
  }
930
1192
  void enqueueWorkerResponse(response);
@@ -939,6 +1201,9 @@ function startWorkerCell(
939
1201
  sessionId: request.sessionId,
940
1202
  cellId: request.cellId,
941
1203
  pendingCalls: [],
1204
+ consoleEntries: [],
1205
+ consoleBytes: 2,
1206
+ consoleOverflow: false,
942
1207
  batchSequence: 0,
943
1208
  callSequence: 0,
944
1209
  batchScheduled: false,