pi-supernova 0.10.2 → 0.11.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 +365 -90
- package/docs/CHANGELOG.md +89 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +38 -18
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +16 -4
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +69 -18
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
package/src/output/final.js
CHANGED
|
@@ -9,11 +9,15 @@ function collectImage(input, acc) {
|
|
|
9
9
|
|
|
10
10
|
acc.imageCount += 1;
|
|
11
11
|
acc.imageBytes += size;
|
|
12
|
+
|
|
12
13
|
if (acc.imageCount > 16 || acc.imageBytes > 20 * 1024 * 1024) {
|
|
13
14
|
acc.imageOverflow = true;
|
|
15
|
+
|
|
14
16
|
return "[image over budget]";
|
|
15
17
|
}
|
|
18
|
+
|
|
16
19
|
const bytes = decodeImageData(input.data);
|
|
20
|
+
|
|
17
21
|
if (input.mimeType === "image/png") assertPng(bytes);
|
|
18
22
|
acc.images.push({ type: "image", data: input.data, mimeType: input.mimeType });
|
|
19
23
|
|
|
@@ -23,12 +27,14 @@ function collectImage(input, acc) {
|
|
|
23
27
|
function displayWeight(value,key) {
|
|
24
28
|
// Count even omitted undefined fields: typed result metadata crosses RPC too.
|
|
25
29
|
const field = isString(key) ? key.length+1 : 0;
|
|
30
|
+
|
|
26
31
|
return field + (isString(value) ? value.length : 1) + (Array.isArray(value) ? value.length : 0);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
function collectImages(input, acc, key) {
|
|
30
35
|
const replaced = collectImage(input, acc);
|
|
31
36
|
acc.displayUnits += displayWeight(replaced ?? input,key);
|
|
37
|
+
|
|
32
38
|
if (replaced !== null) return replaced;
|
|
33
39
|
|
|
34
40
|
return mapChangedChildren(input, collectImages, acc);
|
|
@@ -51,6 +57,7 @@ function clipLogLine(line, maxLogLineChars) {
|
|
|
51
57
|
function clipLogs(logs, config) {
|
|
52
58
|
const maxLines = config.maxLogLines ?? 100;
|
|
53
59
|
let logTruncated = logs.length > maxLines;
|
|
60
|
+
|
|
54
61
|
const clipped = logs.slice(0, maxLines).map(line => {
|
|
55
62
|
const result = clipLogLine(line, config.maxLogLineChars ?? 4096);
|
|
56
63
|
logTruncated ||= result.truncated;
|
|
@@ -67,19 +74,25 @@ function isSourceView(value) {
|
|
|
67
74
|
|
|
68
75
|
function sourcePreview(value, maxReturn) {
|
|
69
76
|
if (!isSourceView(value)) return null;
|
|
77
|
+
|
|
70
78
|
if (value.text.length < maxReturn / 2) return null;
|
|
79
|
+
|
|
71
80
|
if (value.text.length <= maxReturn && formatReturn(value).length <= maxReturn-256) return null;
|
|
72
81
|
const base = {...value,text:"",complete:false,nextOffset:value.lines[0]+1};
|
|
73
82
|
let room = maxReturn - formatReturn(base).length - 512;
|
|
83
|
+
|
|
74
84
|
while (room > 0) {
|
|
75
85
|
const end = value.text.lastIndexOf("\n",room-1) + 1;
|
|
86
|
+
|
|
76
87
|
if (!end || end >= value.text.length) return null;
|
|
77
88
|
const text = value.text.slice(0,end);
|
|
78
89
|
const lines = [value.lines[0],value.lines[0]+text.match(/\n/g).length-1];
|
|
79
90
|
const preview = {...base,text,lines,nextOffset:lines[1]+1};
|
|
91
|
+
|
|
80
92
|
if (formatReturn(preview).length <= maxReturn-256) return preview;
|
|
81
93
|
room = Math.floor(room / 2);
|
|
82
94
|
}
|
|
95
|
+
|
|
83
96
|
return null;
|
|
84
97
|
}
|
|
85
98
|
|
|
@@ -88,23 +101,30 @@ function presentValue(value, maxReturn, oversized) {
|
|
|
88
101
|
const bounded = truncateChars(value,maxReturn,"return");
|
|
89
102
|
const result = serializeReturn(value,formatReturn(bounded.text),maxReturn,false);
|
|
90
103
|
result.truncated ||= bounded.truncated;
|
|
104
|
+
|
|
91
105
|
return result;
|
|
92
106
|
}
|
|
107
|
+
|
|
93
108
|
if (isStringArray(value) && oversized) {
|
|
94
109
|
return {text:formatBoundedStringArray(value,maxReturn),truncated:true};
|
|
95
110
|
}
|
|
111
|
+
|
|
96
112
|
if (oversized) return {text:formatBoundedValue(value,maxReturn),truncated:true};
|
|
113
|
+
|
|
97
114
|
return serializeReturn(value,formatReturn(value),maxReturn,false);
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
export function packageFinalReturn(value, logs, config) {
|
|
101
118
|
const acc = { images: [], imageCount: 0, imageBytes: 0, imageOverflow: false, displayUnits:0 };
|
|
102
119
|
value = collectImages(value, acc);
|
|
120
|
+
|
|
103
121
|
if (acc.imageOverflow) {
|
|
104
122
|
throw new Error(`image attachment budget exceeded: ${acc.imageCount} images / ${acc.imageBytes} bytes; limit is 16 images / 20971520 bytes (20 MiB). No images returned; return fewer or smaller images per program`);
|
|
105
123
|
}
|
|
124
|
+
|
|
106
125
|
const maxReturn = config.maxReturnChars ?? 32000;
|
|
107
126
|
const preview = sourcePreview(value,maxReturn);
|
|
127
|
+
|
|
108
128
|
if (preview) value = preview;
|
|
109
129
|
const serialized = presentValue(value,maxReturn,!preview && acc.displayUnits>maxReturn);
|
|
110
130
|
const clipped = clipLogs(logs, config);
|
package/src/output/format.js
CHANGED
|
@@ -77,9 +77,19 @@ function hasUnpairedSurrogate(text) {
|
|
|
77
77
|
return SURROGATE_CODE_UNIT.test(text) && UNPAIRED_SURROGATE.test(text);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Error messages bypass value formatting. Escape, rather than replace, invalid
|
|
81
|
+
// code units before budgeting so provider text remains well-formed and lossless.
|
|
82
|
+
export function escapeUnpairedSurrogates(text) {
|
|
83
|
+
return hasUnpairedSurrogate(text)
|
|
84
|
+
? text.replace(/[\uD800-\uDFFF]/gu, unit => "\\u" + unit.charCodeAt(0).toString(16).padStart(4, "0"))
|
|
85
|
+
: text;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
export function isStringArray(value) {
|
|
81
89
|
if (!Array.isArray(value) || !value.length) return false;
|
|
90
|
+
|
|
82
91
|
for (const item of value) if (!isString(item)) return false;
|
|
92
|
+
|
|
83
93
|
return true;
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -91,6 +101,7 @@ function hasWellFormedStrings(values) {
|
|
|
91
101
|
|
|
92
102
|
function boundedStringText(value, limit) {
|
|
93
103
|
const text = truncateChars(value,limit,"return").text;
|
|
104
|
+
|
|
94
105
|
return hasUnpairedSurrogate(text) ? truncateChars(JSON.stringify(text),limit,"return").text : text;
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -100,15 +111,18 @@ export function formatBoundedStringArray(values, budget) {
|
|
|
100
111
|
const header = "strings[" + n + "]\n";
|
|
101
112
|
let remaining = Math.max(0, budget - header.length);
|
|
102
113
|
let out = header;
|
|
114
|
+
|
|
103
115
|
if (header.length >= budget) return truncateChars(header,budget,"return").text;
|
|
104
116
|
const omitted = count => "…[return truncated; " + count + " string items omitted]…\n";
|
|
105
117
|
|
|
106
118
|
for (let i = 0; i < n; i++) {
|
|
107
119
|
const itemHeader = "[" + i + "] " + values[i].length + " UTF-16 units\n";
|
|
108
120
|
const footer = i + 1 < n ? omitted(n - i - 1) : "";
|
|
121
|
+
|
|
109
122
|
if (remaining < itemHeader.length + Math.min(32,values[i].length) + 1 + footer.length) {
|
|
110
123
|
return out + truncateChars(omitted(n - i),remaining,"return").text;
|
|
111
124
|
}
|
|
125
|
+
|
|
112
126
|
const per = Math.min(remaining - itemHeader.length - 1 - footer.length, Math.max(32, Math.floor(remaining / (n - i)) - itemHeader.length - 1));
|
|
113
127
|
const chunk = itemHeader + boundedStringText(values[i],per) + "\n";
|
|
114
128
|
remaining = Math.max(0, remaining - chunk.length);
|
|
@@ -181,12 +195,17 @@ const IDENT_KEY = /^[A-Za-z_$][\w$]*$/;
|
|
|
181
195
|
const FORMAT_WIDTH = 120;
|
|
182
196
|
|
|
183
197
|
function formatKey(key) {
|
|
198
|
+
// Computed spelling retains an own data key instead of setting the prototype.
|
|
199
|
+
if (key === "__proto__") return '["__proto__"]';
|
|
200
|
+
|
|
184
201
|
return IDENT_KEY.test(key) ? key : JSON.stringify(key);
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
function formatPrimitive(value) {
|
|
188
205
|
if (value === undefined) return "undefined";
|
|
189
206
|
|
|
207
|
+
if (Object.is(value, -0)) return "-0";
|
|
208
|
+
|
|
190
209
|
if (Number.isNaN(value) || value === Infinity || value === -Infinity) return String(value);
|
|
191
210
|
|
|
192
211
|
try { return JSON.stringify(value) ?? String(value); }
|
|
@@ -291,6 +310,7 @@ export function formatValue(value, indent = "", width = FORMAT_WIDTH, seen = new
|
|
|
291
310
|
if (value?.[RAW_TEXT] !== undefined) return value[RAW_TEXT];
|
|
292
311
|
|
|
293
312
|
if (!isObject(value) && !Array.isArray(value)) return formatPrimitive(value);
|
|
313
|
+
|
|
294
314
|
if (seen.has(value)) return "[Circular]";
|
|
295
315
|
seen.add(value);
|
|
296
316
|
|
|
@@ -314,65 +334,106 @@ function displayKeys(value) {
|
|
|
314
334
|
export function displayExceeds(value, budget) {
|
|
315
335
|
const seen = new Set();
|
|
316
336
|
const spend = units => (budget -= units) < 0;
|
|
337
|
+
|
|
317
338
|
function container(item) {
|
|
318
339
|
if (seen.has(item)) return false;
|
|
319
340
|
seen.add(item);
|
|
320
341
|
const array = Array.isArray(item);
|
|
342
|
+
|
|
321
343
|
if (array && spend(item.length)) return true;
|
|
344
|
+
|
|
322
345
|
for (const key of displayKeys(item)) {
|
|
323
346
|
if (!array && spend(key.length+1)) return true;
|
|
347
|
+
|
|
324
348
|
if (visit(item[key])) return true;
|
|
325
349
|
}
|
|
350
|
+
|
|
326
351
|
seen.delete(item);
|
|
352
|
+
|
|
327
353
|
return false;
|
|
328
354
|
}
|
|
355
|
+
|
|
329
356
|
function visit(item) {
|
|
330
357
|
if (spend(isString(item) ? item.length : 1)) return true;
|
|
358
|
+
|
|
331
359
|
return isObject(item) || Array.isArray(item) ? container(item) : false;
|
|
332
360
|
}
|
|
361
|
+
|
|
333
362
|
return visit(value);
|
|
334
363
|
}
|
|
335
364
|
|
|
336
|
-
|
|
365
|
+
function boundedQuotedText(text, budget, label) {
|
|
366
|
+
if (budget < 2) return "";
|
|
367
|
+
const end = maxJsonStringPrefix(text, budget);
|
|
368
|
+
|
|
369
|
+
if (end === text.length) return JSON.stringify(text);
|
|
370
|
+
const marker = "…[" + label + " truncated; " + text.length + " UTF-16 units]…";
|
|
371
|
+
const markerChars = JSON.stringify(marker).length - 2;
|
|
372
|
+
|
|
373
|
+
if (markerChars + 2 > budget) return JSON.stringify(text.slice(0, end));
|
|
374
|
+
const prefix = maxJsonStringPrefix(text, budget - markerChars);
|
|
375
|
+
|
|
376
|
+
return JSON.stringify(text.slice(0, prefix) + marker);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Fair display-only previews; bounded work, no expansion of full strings. */
|
|
337
380
|
export function formatBoundedValue(value, budget, label = "return") {
|
|
338
|
-
const footer = "\n…["+label+" truncated]…";
|
|
339
|
-
const
|
|
381
|
+
const footer = "\n…[" + label + " truncated]…";
|
|
382
|
+
const limit = charLimit(budget, 32000);
|
|
340
383
|
const seen = new Set();
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
}
|
|
351
|
-
function visitChild(item,key,array) {
|
|
352
|
-
if (!array) { quoted(key); push(":"); }
|
|
353
|
-
visit(array && item[key] === undefined ? null : item[key]);
|
|
354
|
-
}
|
|
355
|
-
function container(item) {
|
|
356
|
-
if (seen.has(item)) { push('"[Circular]"'); return; }
|
|
384
|
+
|
|
385
|
+
function render(item, room, depth) {
|
|
386
|
+
if (isString(item)) return boundedQuotedText(item, room, label);
|
|
387
|
+
|
|
388
|
+
if (!isObject(item) && !Array.isArray(item)) return truncateChars(formatPrimitive(item), room, label).text;
|
|
389
|
+
|
|
390
|
+
if (seen.has(item)) return truncateChars('"[Circular]"', room, label).text;
|
|
391
|
+
|
|
392
|
+
if (depth >= 32 || room < 4) return truncateChars("…[" + label + " truncated]…", room, label).text;
|
|
357
393
|
seen.add(item);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (!first) push(",");
|
|
364
|
-
first = false;
|
|
365
|
-
visitChild(item,key,array);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
return container(item, room, depth);
|
|
397
|
+
} finally {
|
|
398
|
+
seen.delete(item);
|
|
366
399
|
}
|
|
367
|
-
if (!stopped) push(array ? "]" : "}");
|
|
368
|
-
seen.delete(item);
|
|
369
400
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
401
|
+
|
|
402
|
+
function container(item, room, depth) {
|
|
403
|
+
const array = Array.isArray(item);
|
|
404
|
+
const keys = displayKeys(item);
|
|
405
|
+
const count = array ? item.length : keys.length;
|
|
406
|
+
const parts = [array ? "[" : "{"];
|
|
407
|
+
let remaining = room - 2;
|
|
408
|
+
let index = 0;
|
|
409
|
+
|
|
410
|
+
for (const key of keys) {
|
|
411
|
+
const separator = index === 0 ? "" : ",";
|
|
412
|
+
const heading = array ? "" : boundedQuotedText(key, Math.max(2, Math.floor(remaining / 3)), "key") + ":";
|
|
413
|
+
const omitted = "…[" + label + " truncated; " + (count - index) + " items omitted]…";
|
|
414
|
+
const reserve = index + 1 < count ? omitted.length + 1 : 0;
|
|
415
|
+
const available = remaining - separator.length - heading.length - reserve;
|
|
416
|
+
|
|
417
|
+
if (available < 4) {
|
|
418
|
+
parts.push(truncateChars(omitted, remaining, label).text);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Small siblings give unused space back. Large siblings cannot consume
|
|
423
|
+
// later paths/results; very large collections disclose the omitted count.
|
|
424
|
+
const share = Math.min(available, Math.max(64, Math.floor(remaining / (count - index)) - heading.length - separator.length));
|
|
425
|
+
const child = array && item[key] === undefined ? null : item[key];
|
|
426
|
+
const text = separator + heading + render(child, share, depth + 1);
|
|
427
|
+
|
|
428
|
+
parts.push(text);
|
|
429
|
+
remaining -= text.length;
|
|
430
|
+
index += 1;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
parts.push(array ? "]" : "}");
|
|
434
|
+
|
|
435
|
+
return parts.join("");
|
|
375
436
|
}
|
|
376
|
-
|
|
377
|
-
return
|
|
437
|
+
|
|
438
|
+
return render(value, Math.max(0, limit - footer.length), 0) + truncateChars(footer, limit, label).text;
|
|
378
439
|
}
|
package/src/output/outcome.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {isString} from '../shared/decode.js';
|
|
2
|
-
import {truncateChars,formatBoundedStringArray,isStringArray} from './format.js';
|
|
2
|
+
import {truncateChars,formatBoundedStringArray,isStringArray,escapeUnpairedSurrogates} from './format.js';
|
|
3
3
|
|
|
4
4
|
function result(text, details) {
|
|
5
5
|
return { content: [{ type: "text", text }], details };
|
|
@@ -24,7 +24,10 @@ function mutationText(outcome, failed) {
|
|
|
24
24
|
|
|
25
25
|
if (!m.committed && !m.rolledBack && !external && !uncertain) return "";
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const saved = failed && outcome.savedPaths?.length ? "\nSaved edits: " + truncateChars(JSON.stringify(outcome.savedPaths), 2048, "saved paths").text : "";
|
|
28
|
+
const retry = failed && m.committed && !uncertain ? saved + "\nDo not repeat saved edits. Retry only failed or not-run work; dependent JavaScript stops at the uncaught error." : saved;
|
|
29
|
+
|
|
30
|
+
return "\nmutations: committed=" + (m.committed || 0) + " rolledBack=" + (m.rolledBack || 0) + " (file versions)" + external + uncertain + retry;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function mutationReceipts(trace) {
|
|
@@ -45,8 +48,8 @@ function splitTurnHint(outcome) {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function errorText(outcome, call) {
|
|
48
|
-
return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome, true)}${splitTurnHint(outcome)}
|
|
49
|
-
error: ${outcome.error}${logsBlock(outcome)}
|
|
51
|
+
return escapeUnpairedSurrogates(`error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome, true)}${splitTurnHint(outcome)}
|
|
52
|
+
error: ${outcome.error}${logsBlock(outcome)}`);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function successText(outcome, call) {
|
|
@@ -76,6 +79,14 @@ function fitOutput(outcome, call, limit, format) {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function attachReceipts(outcome, trace) {
|
|
82
|
+
const saved = new Set();
|
|
83
|
+
|
|
84
|
+
for (const row of trace) {
|
|
85
|
+
if (row.mutationState === "saved" && isString(row.args?.path)) saved.add(row.args.path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
outcome.savedPaths = [...saved];
|
|
89
|
+
|
|
79
90
|
if (outcome.ok && outcome.result === undefined) {
|
|
80
91
|
const receipts = mutationReceipts(trace);
|
|
81
92
|
|
|
@@ -92,4 +103,5 @@ function throwIfFailed(outcome, visible, response) {
|
|
|
92
103
|
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
93
104
|
throw error;
|
|
94
105
|
}
|
|
106
|
+
|
|
95
107
|
export { result, errorText, successText, fitOutput, attachReceipts, throwIfFailed };
|
|
@@ -2,6 +2,7 @@ import {isObject,isString} from '../shared/decode.js';
|
|
|
2
2
|
|
|
3
3
|
function assertProgramEntry(p, defaults = {}) {
|
|
4
4
|
const source = p?.code === undefined && p?.file === undefined ? defaults : p;
|
|
5
|
+
|
|
5
6
|
if (!objectData(p) || Object.keys(p).some(key => !["code", "file", "data"].includes(key)) || !validProgramSource(source)) {
|
|
6
7
|
throw new Error("each program requires code OR file (own or shared), with optional data; no nested batches or per-entry timeouts; no programs ran");
|
|
7
8
|
}
|
|
@@ -30,11 +31,17 @@ function applyBatchDefaults(parsed, mergeData) {
|
|
|
30
31
|
|
|
31
32
|
function parseBatchPayload(params, config) {
|
|
32
33
|
assertBatchOptions(params);
|
|
33
|
-
const defaults = Object.fromEntries(["code", "file", "data"].
|
|
34
|
+
const defaults = Object.fromEntries(["code", "file", "data"].flatMap(key => (params[key] !== undefined ? [[key, params[key]]] : [])));
|
|
35
|
+
|
|
34
36
|
if (defaults.code !== undefined || defaults.file !== undefined) assertProgramEntry(defaults);
|
|
37
|
+
|
|
35
38
|
for (const p of params.programs) assertProgramEntry(p, defaults);
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
// Serialization may change the array itself as well as its entries.
|
|
41
|
+
const parsed = snapshotBatch(params, defaults, config);
|
|
42
|
+
assertBatchOptions(parsed);
|
|
43
|
+
|
|
44
|
+
return applyBatchDefaults(parsed, params.mergeData === true);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function batchTimeoutMs(params, config) {
|
|
@@ -47,22 +54,29 @@ function batchTimeoutMs(params, config) {
|
|
|
47
54
|
|
|
48
55
|
function validProgramSource(source) {
|
|
49
56
|
const value = source.code ?? source.file;
|
|
57
|
+
|
|
50
58
|
return (source.code === undefined) !== (source.file === undefined) && isString(value) && value.trim();
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
function assertBatchOptions(params) {
|
|
54
62
|
if (!Array.isArray(params.programs) || !params.programs.length || params.programs.length > 32) throw new Error("programs requires 1..32 entries; no programs ran");
|
|
63
|
+
|
|
55
64
|
if (params.mergeData !== undefined && ![true, false].includes(params.mergeData)) throw new Error("mergeData must be boolean; no programs ran");
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
function snapshotBatch(params, defaults, config) {
|
|
59
68
|
const hasDefaults = Object.keys(defaults).length > 0;
|
|
60
69
|
let encoded;
|
|
70
|
+
|
|
61
71
|
try { encoded = JSON.stringify(hasDefaults ? {programs:params.programs,...defaults} : params.programs); }
|
|
62
72
|
catch { throw new Error("programs and defaults must be JSON-serializable; no programs ran"); }
|
|
73
|
+
|
|
63
74
|
if (encoded.length > (config.maxCodeChars ?? 48000)) throw new Error("programs JSON exceeds the code character budget (including shared code/file/data); no programs ran");
|
|
64
75
|
const parsed = hasDefaults ? JSON.parse(encoded) : {programs:JSON.parse(encoded)};
|
|
76
|
+
|
|
65
77
|
if (Object.hasOwn(defaults,"data") && !Object.hasOwn(parsed,"data")) throw new Error("data must be JSON-serializable; no programs ran");
|
|
78
|
+
|
|
66
79
|
return parsed;
|
|
67
80
|
}
|
|
81
|
+
|
|
68
82
|
export { parseBatchPayload, batchTimeoutMs };
|
package/src/runtime/guest-api.js
CHANGED
|
@@ -47,6 +47,14 @@ function leanEnvelope(res) {
|
|
|
47
47
|
return res;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// RPC and coalesced-read rejection callbacks run outside the awaiting guest's
|
|
51
|
+
// stack. Recreate their error at this async boundary, only on failure, so V8 can
|
|
52
|
+
// retain the guest await frames. Do not capture stacks on successful calls.
|
|
53
|
+
async function withGuestStack(promise) {
|
|
54
|
+
try { return await promise; }
|
|
55
|
+
catch (error) { throw new Error(error.message, { cause: error.cause }); }
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
function swallow(promise) {
|
|
51
59
|
promise.catch(() => {});
|
|
52
60
|
|
|
@@ -192,7 +200,10 @@ function formatBashFailure(command, res) {
|
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
function markTruncatedOutput(res, text) {
|
|
195
|
-
|
|
203
|
+
// A literal word such as a variable name is not a truncation disclosure.
|
|
204
|
+
const marker = "…[output truncated]…";
|
|
205
|
+
|
|
206
|
+
if (res?.truncated && isString(text) && !text.endsWith(marker)) return text + "\n" + marker;
|
|
196
207
|
|
|
197
208
|
return text;
|
|
198
209
|
}
|
|
@@ -240,7 +251,7 @@ export function buildGuestApi(rpc, batchRead) {
|
|
|
240
251
|
const invoke = (name, args) => {
|
|
241
252
|
assertScope(); flushReads();
|
|
242
253
|
|
|
243
|
-
return swallow(rpc("call", [name, args]).then(leanEnvelope));
|
|
254
|
+
return swallow(withGuestStack(rpc("call", [name, args]).then(leanEnvelope)));
|
|
244
255
|
};
|
|
245
256
|
|
|
246
257
|
const read = async (p, a, b) => {
|
|
@@ -250,9 +261,13 @@ export function buildGuestApi(rpc, batchRead) {
|
|
|
250
261
|
|
|
251
262
|
if (Array.isArray(p)) return await readManyPaths(read, args, p);
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
// Preserve streamed-item timing by restoring failures in this existing
|
|
265
|
+
// async boundary, not through another successful-path Promise hop.
|
|
266
|
+
try {
|
|
267
|
+
return !batchRead
|
|
268
|
+
? unwrapRead(await invoke("read", args), args, noteReadPath)
|
|
269
|
+
: await enqueueCompatibleRead(readState, flushReads, args);
|
|
270
|
+
} catch (error) { throw new Error(error.message, { cause: error.cause }); }
|
|
256
271
|
};
|
|
257
272
|
|
|
258
273
|
const readFiles = new Set();
|
|
@@ -265,8 +280,18 @@ export function buildGuestApi(rpc, batchRead) {
|
|
|
265
280
|
if (isString(envelope.sourcePath)) readFiles.add(envelope.sourcePath);
|
|
266
281
|
}
|
|
267
282
|
|
|
268
|
-
const write = async (p, content) => {
|
|
269
|
-
|
|
283
|
+
const write = async (p, content, options) => {
|
|
284
|
+
if (isObject(p) && (content !== undefined || options !== undefined)) throw new Error("write object form does not accept positional content or options");
|
|
285
|
+
|
|
286
|
+
if (options !== undefined && (!isObject(options) || Object.keys(options).some(key => !["append", "replace", "allowReadArtifacts"].includes(key)))) {
|
|
287
|
+
throw new Error("write options must be an object containing only append, replace, allowReadArtifacts");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const args = isObject(p) ? p : { ...options, path: p, content };
|
|
291
|
+
|
|
292
|
+
for (const key of ["append", "replace", "allowReadArtifacts"]) {
|
|
293
|
+
if (args[key] !== undefined && args[key] !== true && args[key] !== false) throw new Error("write " + key + " must be a boolean");
|
|
294
|
+
}
|
|
270
295
|
|
|
271
296
|
if (args.append !== true && args.replace !== true && isString(args.path) && readFiles.has(args.path)) {
|
|
272
297
|
throw new Error("file was already read this program; use edit(oldText, newText) or edit(view, ...). write({path,content,replace:true}) replaces anyway");
|
|
@@ -9,6 +9,7 @@ import { truncateChars,formatReturn,displayExceeds,formatBoundedValue } from "..
|
|
|
9
9
|
import { guestImportMessage, isDeniedGuestImport } from "./guest-deny-imports.js";
|
|
10
10
|
|
|
11
11
|
const { register, registerHooks } = nodeModule;
|
|
12
|
+
|
|
12
13
|
// heapUsed/external belong to this worker; rss includes the entire Pi/OMP host.
|
|
13
14
|
const memoryUsage = process.memoryUsage.bind(process);
|
|
14
15
|
|
|
@@ -97,12 +98,16 @@ function encodeConsoleArg(a, limit) {
|
|
|
97
98
|
const raw = truncateChars(a,limit,"log");
|
|
98
99
|
const encoded = truncateChars(formatReturn(raw.text),limit,"log");
|
|
99
100
|
encoded.truncated ||= raw.truncated;
|
|
101
|
+
|
|
100
102
|
return encoded;
|
|
101
103
|
}
|
|
104
|
+
|
|
102
105
|
try {
|
|
103
106
|
const plain = toPlain(a);
|
|
107
|
+
|
|
104
108
|
if (displayExceeds(plain,limit)) return {text:formatBoundedValue(plain,limit,"log"),truncated:true};
|
|
105
109
|
const encoded = JSON.stringify(plain);
|
|
110
|
+
|
|
106
111
|
return truncateChars(encoded === undefined ? String(plain) : encoded,limit,"log");
|
|
107
112
|
} catch { return truncateChars(formatReturn(String(a)),limit,"log"); }
|
|
108
113
|
}
|
|
@@ -130,10 +135,13 @@ function handleRpcResult(msg) {
|
|
|
130
135
|
|
|
131
136
|
function handleReadItem(msg) {
|
|
132
137
|
const pending = pendingRpc.get(msg.id);
|
|
138
|
+
|
|
133
139
|
if (!pending?.onItem || !runActive || msg.runId !== activeRunId) return;
|
|
134
140
|
let error;
|
|
141
|
+
|
|
135
142
|
try { pending.onItem(msg.index,msg.value); }
|
|
136
143
|
catch (err) { error = errorMessage(err); pending.reject(err); pendingRpc.delete(msg.id); }
|
|
144
|
+
|
|
137
145
|
reportMemory(activeRunId);
|
|
138
146
|
post({op:"rpc:ack",id:msg.id,index:msg.index,runId:activeRunId,error});
|
|
139
147
|
}
|
|
@@ -154,14 +162,19 @@ function makeConsole(runId, limits) {
|
|
|
154
162
|
|
|
155
163
|
count++;
|
|
156
164
|
let line = "", first = true;
|
|
165
|
+
|
|
157
166
|
for (const arg of args) {
|
|
158
167
|
const room = limits.maxLogLineChars-line.length-(first ? 0 : 1);
|
|
168
|
+
|
|
159
169
|
if (room <= 0) { markTruncated(); break; }
|
|
170
|
+
|
|
160
171
|
const encoded = encodeConsoleArg(arg,room);
|
|
172
|
+
|
|
161
173
|
if (encoded.truncated) markTruncated();
|
|
162
174
|
line += (first ? "" : " ")+encoded.text;
|
|
163
175
|
first = false;
|
|
164
176
|
}
|
|
177
|
+
|
|
165
178
|
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
166
179
|
|
|
167
180
|
if (clipped.truncated) markTruncated();
|
|
@@ -171,10 +184,10 @@ function makeConsole(runId, limits) {
|
|
|
171
184
|
return { log: emit, warn: emit, error: emit, info: emit, debug: emit };
|
|
172
185
|
}
|
|
173
186
|
|
|
174
|
-
function postFailure(runId, err, location) {
|
|
187
|
+
function postFailure(runId, err, location, errorKind = err?.name) {
|
|
175
188
|
runActive = false;
|
|
176
189
|
const message = errorMessage(err);
|
|
177
|
-
post({ op: "error", runId, message, location });
|
|
190
|
+
post({ op: "error", runId, message, location, errorKind });
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
function denyBuiltin(id) {
|
|
@@ -186,6 +199,7 @@ let sealedRealm = false;
|
|
|
186
199
|
function sealGuestRealm() {
|
|
187
200
|
if (sealedRealm) return;
|
|
188
201
|
sealedRealm = true;
|
|
202
|
+
|
|
189
203
|
if (isFunction(process.getBuiltinModule)) {
|
|
190
204
|
const orig = process.getBuiltinModule.bind(process);
|
|
191
205
|
process.getBuiltinModule = (id) => {
|
|
@@ -227,7 +241,7 @@ async function handleRun(msg) {
|
|
|
227
241
|
// Existing programs may declare their own data variable; bind it only when supplied.
|
|
228
242
|
compiled = compileGuest(prepared, msg.data);
|
|
229
243
|
} catch (err) {
|
|
230
|
-
postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran."));
|
|
244
|
+
postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran."), undefined, err.name);
|
|
231
245
|
|
|
232
246
|
return;
|
|
233
247
|
}
|
|
@@ -258,7 +272,11 @@ async function handleRun(msg) {
|
|
|
258
272
|
parentPort.on("message", (msg) => {
|
|
259
273
|
if (!isObject(msg)) return;
|
|
260
274
|
|
|
261
|
-
if (msg.op === "rpc:item") {
|
|
275
|
+
if (msg.op === "rpc:item") {
|
|
276
|
+
handleReadItem(msg);
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
262
280
|
|
|
263
281
|
if (msg.op === "rpc:result") {
|
|
264
282
|
handleRpcResult(msg);
|