@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.
- package/README.md +96 -24
- package/package.json +6 -6
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +19 -38
- package/src/codemode-session-coordinator.ts +313 -158
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +108 -14
- package/src/codemode-tool-rendering.ts +138 -39
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +31 -28
- package/src/pi-tool-bridge.ts +5 -38
|
@@ -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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
420
|
-
?
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" ||
|
package/src/codemode-worker.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
911
|
-
|
|
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-
|
|
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 =
|
|
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
|
-
|
|
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,
|