pi-supernova 0.0.15 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +87 -33
- package/bottleneck.js +98 -97
- package/catalog.js +7 -32
- package/config.js +1 -2
- package/decode.js +61 -0
- package/evidence.js +14 -5
- package/format.js +16 -17
- package/guest-worker.js +29 -150
- package/host-bridge.js +152 -67
- package/index.js +82 -101
- package/ledger.js +73 -101
- package/omp-frame.js +0 -1
- package/package.json +7 -3
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +48 -118
- package/render.js +12 -13
- package/repo-index.js +16 -7
- package/runtime.js +204 -210
- package/snap.js +183 -214
- package/vfs.js +114 -70
- package/workspace.js +37 -33
package/decode.js
CHANGED
|
@@ -4,3 +4,64 @@ export const isString = (v) => toStr.call(v) === "[object String]";
|
|
|
4
4
|
export const isObject = (v) => toStr.call(v) === "[object Object]";
|
|
5
5
|
export const isFunction = (v) => toStr.call(v) === "[object Function]" || v instanceof Function;
|
|
6
6
|
export const isNumber = (v) => toStr.call(v) === "[object Number]" && Number.isFinite(v);
|
|
7
|
+
|
|
8
|
+
const MAX_DEPTH = 64;
|
|
9
|
+
const MAX_TYPED_ARRAY = 4096;
|
|
10
|
+
|
|
11
|
+
function plainFromBinary(value) {
|
|
12
|
+
const bytes = value.byteLength;
|
|
13
|
+
if (value instanceof ArrayBuffer) value = new Uint8Array(value);
|
|
14
|
+
else if (value instanceof DataView) value = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
15
|
+
if (value.length > MAX_TYPED_ARRAY) return "[" + value.constructor.name + " " + bytes + " bytes]";
|
|
16
|
+
return value instanceof BigInt64Array || value instanceof BigUint64Array
|
|
17
|
+
? Array.from(value, x => x.toString() + "n")
|
|
18
|
+
: Array.from(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function plainFromMap(value, seen, depth) {
|
|
22
|
+
const allStringKeys = [...value.keys()].every(isString);
|
|
23
|
+
if (!allStringKeys) return [...value].map(([k, v]) => [toPlain(k, seen, depth + 1), toPlain(v, seen, depth + 1)]);
|
|
24
|
+
const out = Object.create(null);
|
|
25
|
+
for (const [k, v] of value) out[k] = toPlain(v, seen, depth + 1);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function plainFromCollection(value, seen, depth) {
|
|
30
|
+
if (Array.isArray(value)) return value.map((x) => toPlain(x, seen, depth + 1));
|
|
31
|
+
if (value instanceof Set) return [...value].map((x) => toPlain(x, seen, depth + 1));
|
|
32
|
+
if (value instanceof Map) return plainFromMap(value, seen, depth);
|
|
33
|
+
const out = Object.create(null);
|
|
34
|
+
for (const k of Object.keys(value)) out[k] = toPlain(value[k], seen, depth + 1);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convert any guest value to structured-clone-safe, JSON-shaped data. */
|
|
39
|
+
export function toPlain(value, seen = new Set(), depth = 0) {
|
|
40
|
+
if (value === null || value === undefined) return value;
|
|
41
|
+
const tag = toStr.call(value);
|
|
42
|
+
if (tag === "[object String]" || tag === "[object Number]" || tag === "[object Boolean]") return value.valueOf();
|
|
43
|
+
if (tag === "[object BigInt]") return value.toString() + "n";
|
|
44
|
+
if (isFunction(value)) return "[Function" + (value.name ? " " + value.name : "") + "]";
|
|
45
|
+
if (tag === "[object Symbol]") return value.toString();
|
|
46
|
+
if (depth > MAX_DEPTH) return "[Depth]";
|
|
47
|
+
if (seen.has(value)) return "[Circular]";
|
|
48
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
49
|
+
if (value instanceof RegExp) return value.toString();
|
|
50
|
+
if (value instanceof Error) {
|
|
51
|
+
const out = { name: value.name, message: value.message };
|
|
52
|
+
if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
if (value instanceof Promise) return "[Promise]";
|
|
56
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
|
|
57
|
+
if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
|
|
58
|
+
seen.add(value);
|
|
59
|
+
try {
|
|
60
|
+
return plainFromCollection(value, seen, depth);
|
|
61
|
+
} finally {
|
|
62
|
+
seen.delete(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- RPC to the host thread ----
|
|
67
|
+
|
package/evidence.js
CHANGED
|
@@ -309,7 +309,7 @@ function normalize(scores) {
|
|
|
309
309
|
|
|
310
310
|
// ---- candidate files (boundary + topology + entity hits) ----
|
|
311
311
|
|
|
312
|
-
function candidateFiles(files, profile, index, limit) {
|
|
312
|
+
function candidateFiles(files, profile, index, limit, overlayText) {
|
|
313
313
|
const scored = [];
|
|
314
314
|
for (const f of files) {
|
|
315
315
|
const s = scorePathTopology(f, profile.keywords, profile.flags);
|
|
@@ -318,7 +318,11 @@ function candidateFiles(files, profile, index, limit) {
|
|
|
318
318
|
scored.sort((a, b) => b.s - a.s);
|
|
319
319
|
const chosen = new Set(scored.slice(0, limit).map(({ f }) => f));
|
|
320
320
|
const anchors = (profile.subjects.length ? profile.subjects : profile.keywords).map((a) => a.toLowerCase()).filter((a) => a.length > 2);
|
|
321
|
-
const
|
|
321
|
+
const pendingHits = files.filter(file => {
|
|
322
|
+
const pending = overlayText(file);
|
|
323
|
+
return pending !== undefined && anchors.some(anchor => pending.toLowerCase().includes(anchor));
|
|
324
|
+
});
|
|
325
|
+
const hits = anchors.length ? [...new Set([...pendingHits, ...index.filesContaining(files, anchors, true)])] : [];
|
|
322
326
|
for (const f of hits) {
|
|
323
327
|
if (chosen.size >= limit) break;
|
|
324
328
|
if (profile.flags.wantsTest || scorePathTopology(f, profile.keywords, profile.flags) > -50) chosen.add(f);
|
|
@@ -385,14 +389,19 @@ function render(spans, picks, fused, opts, root) {
|
|
|
385
389
|
* R(q): top-K provenance-bearing source spans for a concept query, selected without any model call.
|
|
386
390
|
* @returns {{ route: string, spans: Array<{path, lines, name, kind, why, text}> }}
|
|
387
391
|
*/
|
|
388
|
-
export async function selectEvidence({ query, root, searchDir, index, overlayText = () => undefined, options = {} }) {
|
|
392
|
+
export async function selectEvidence({ query, root, searchDir, index, overlayText = () => undefined, pendingPaths = [], options = {} }) {
|
|
389
393
|
const opts = { ...EVIDENCE_DEFAULTS, ...options };
|
|
390
394
|
const profile = profileQuery(query);
|
|
391
395
|
if (profile.keywords.length === 0) throw new Error("evidence requires at least one searchable concept keyword");
|
|
392
|
-
const
|
|
396
|
+
const searchRoot = path.resolve(searchDir || root);
|
|
397
|
+
const staged = pendingPaths.filter(file => {
|
|
398
|
+
const relative = path.relative(searchRoot, file);
|
|
399
|
+
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
400
|
+
});
|
|
401
|
+
const files = [...new Set([...await index.files(searchRoot), ...staged])];
|
|
393
402
|
if (files.length === 0) throw new Error(`no files found to search in ${searchDir || root}`);
|
|
394
403
|
|
|
395
|
-
const { files: chosenFiles, fileScores } = candidateFiles(files, profile, index, opts.maxCandidateFiles);
|
|
404
|
+
const { files: chosenFiles, fileScores } = candidateFiles(files, profile, index, opts.maxCandidateFiles, overlayText);
|
|
396
405
|
const spans = [];
|
|
397
406
|
for (const f of chosenFiles) {
|
|
398
407
|
const pending = overlayText(f);
|
package/format.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isString } from "./decode.js";
|
|
1
|
+
import { isString, isObject } from "./decode.js";
|
|
2
2
|
|
|
3
3
|
export function truncateChars(text, maxChars, label = "value") {
|
|
4
4
|
const normalized = isString(text) ? text : String(text ?? "");
|
|
@@ -12,22 +12,21 @@ export function truncateChars(text, maxChars, label = "value") {
|
|
|
12
12
|
originalChars: normalized.length,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
let tail =
|
|
15
|
+
let head = headEnd(normalized, Math.floor(limit * 0.7));
|
|
16
|
+
let tail = 0;
|
|
17
17
|
let marker = "";
|
|
18
|
-
|
|
19
|
-
while (tail !== previousTail) {
|
|
20
|
-
previousTail = tail;
|
|
18
|
+
for (;;) {
|
|
21
19
|
const omitted = normalized.length - head - tail;
|
|
22
|
-
marker =
|
|
23
|
-
|
|
20
|
+
marker = "\n…[" + label + " truncated " + omitted + " chars]…\n";
|
|
21
|
+
if (marker.length > limit) return { text: normalized.slice(0, headEnd(normalized, limit)), truncated: true, originalChars: normalized.length };
|
|
22
|
+
const budget = limit - marker.length;
|
|
23
|
+
const nextHead = headEnd(normalized, Math.min(head, budget));
|
|
24
|
+
const nextTail = normalized.length - tailStartIndex(normalized, Math.max(0, budget - nextHead));
|
|
25
|
+
if (nextHead === head && nextTail === tail) break;
|
|
26
|
+
head = nextHead;
|
|
27
|
+
tail = nextTail;
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
text: normalized.slice(0, head) + marker + normalized.slice(tailStart),
|
|
28
|
-
truncated: true,
|
|
29
|
-
originalChars: normalized.length,
|
|
30
|
-
};
|
|
29
|
+
return { text: normalized.slice(0, head) + marker + normalized.slice(normalized.length - tail), truncated: true, originalChars: normalized.length };
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
// Lone surrogates in a tool result make the message invalid UTF-8 at the API
|
|
@@ -53,7 +52,7 @@ function formatKey(key) {
|
|
|
53
52
|
|
|
54
53
|
function formatPrimitive(value) {
|
|
55
54
|
if (value === undefined) return "undefined";
|
|
56
|
-
if (
|
|
55
|
+
if (Number.isNaN(value) || value === Infinity || value === -Infinity) return String(value);
|
|
57
56
|
return JSON.stringify(value) ?? String(value);
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -65,7 +64,7 @@ function formatFlatList(value) {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
function formatFlat(value) {
|
|
68
|
-
if (value
|
|
67
|
+
if ((!isObject(value) && !Array.isArray(value))) return formatPrimitive(value);
|
|
69
68
|
if (Array.isArray(value)) return formatFlatList(value);
|
|
70
69
|
let out = "";
|
|
71
70
|
for (const key of Object.keys(value)) {
|
|
@@ -83,7 +82,7 @@ function formatFlat(value) {
|
|
|
83
82
|
*/
|
|
84
83
|
export function formatValue(value, indent = "", width = FORMAT_WIDTH) {
|
|
85
84
|
const flat = formatFlat(value);
|
|
86
|
-
if (value
|
|
85
|
+
if ((!isObject(value) && !Array.isArray(value)) || flat.length + indent.length <= width) return flat;
|
|
87
86
|
const pad = indent + " ";
|
|
88
87
|
if (Array.isArray(value)) {
|
|
89
88
|
if (value.length === 0) return "[]";
|
package/guest-worker.js
CHANGED
|
@@ -1,95 +1,15 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
2
|
import { parallel as runParallel, pipeline as runPipeline } from "./parallel.js";
|
|
3
|
-
import { isString,
|
|
3
|
+
import { isString, isObject, toPlain } from "./decode.js";
|
|
4
|
+
import { truncateChars } from "./format.js";
|
|
4
5
|
|
|
5
6
|
// Guest programs run here, off the host thread. The host can terminate() this
|
|
6
7
|
// worker mid-loop, so a runaway "while (true) {}" or process.exit() in guest
|
|
7
8
|
// code cannot take the harness down with it.
|
|
8
9
|
|
|
9
10
|
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
10
|
-
const
|
|
11
|
-
const COMPILED_CACHE_MAX = 256;
|
|
12
|
-
const PARAMS = [
|
|
13
|
-
"nova", "console", "parallel", "pipeline",
|
|
14
|
-
"read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate",
|
|
15
|
-
];
|
|
16
|
-
// V8 and JSC both place the body on the line after the synthesized header.
|
|
11
|
+
const PARAMS = ["nova", "console", "parallel", "pipeline", "read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate"];
|
|
17
12
|
const BODY_LINE_OFFSET = 2;
|
|
18
|
-
|
|
19
|
-
function skipLeadingComments(src) {
|
|
20
|
-
let i = 0;
|
|
21
|
-
for (;;) {
|
|
22
|
-
while (/\s/.test(src[i])) i++;
|
|
23
|
-
if (src.startsWith("//", i)) {
|
|
24
|
-
const nl = src.indexOf("\n", i);
|
|
25
|
-
if (nl < 0) return src.length;
|
|
26
|
-
i = nl + 1;
|
|
27
|
-
} else if (src.startsWith("/*", i)) {
|
|
28
|
-
const end = src.indexOf("*/", i + 2);
|
|
29
|
-
if (end < 0) return src.length;
|
|
30
|
-
i = end + 2;
|
|
31
|
-
} else {
|
|
32
|
-
return i;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function skipString(src, j, quote) {
|
|
38
|
-
for (j++; j < src.length && src[j] !== quote; j++) if (src[j] === "\\") j++;
|
|
39
|
-
return j;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function skipBalancedParens(src, i) {
|
|
43
|
-
let depth = 0;
|
|
44
|
-
for (let j = i; j < src.length; j++) {
|
|
45
|
-
const ch = src[j];
|
|
46
|
-
if (ch === "(") {
|
|
47
|
-
depth++;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (ch === ")") {
|
|
51
|
-
depth--;
|
|
52
|
-
if (depth === 0) return j + 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if ("\"'\u0060".includes(ch)) j = skipString(src, j, ch);
|
|
56
|
-
}
|
|
57
|
-
return -1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** True when src (after comments) is a single arrow or function expression. */
|
|
61
|
-
function isFunctionExpression(src) {
|
|
62
|
-
let i = skipLeadingComments(src);
|
|
63
|
-
const rest = src.slice(i);
|
|
64
|
-
if (/^(async\s+)?function\b/.test(rest)) return true;
|
|
65
|
-
const asyncMatch = /^async\s+/.exec(rest);
|
|
66
|
-
if (asyncMatch) i += asyncMatch[0].length;
|
|
67
|
-
if (/^[A-Za-z_$][\w$]*\s*=>/.test(src.slice(i))) return true;
|
|
68
|
-
if (src[i] !== "(") return false;
|
|
69
|
-
const after = skipBalancedParens(src, i);
|
|
70
|
-
if (after < 0) return false;
|
|
71
|
-
return /^\s*=>/.test(src.slice(after));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function wrapBody(code) {
|
|
75
|
-
const trimmed = String(code).trim();
|
|
76
|
-
if (!trimmed) throw new Error("code must be a non-empty string");
|
|
77
|
-
if (isFunctionExpression(trimmed)) return "const __fn = (" + trimmed + ");\nreturn await __fn();";
|
|
78
|
-
return trimmed;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function compile(code) {
|
|
82
|
-
const body = wrapBody(code);
|
|
83
|
-
let compiled = compiledCache.get(body);
|
|
84
|
-
if (compiled) return compiled;
|
|
85
|
-
compiled = new AsyncFunction(...PARAMS, body);
|
|
86
|
-
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
87
|
-
compiledCache.delete(compiledCache.keys().next().value);
|
|
88
|
-
}
|
|
89
|
-
compiledCache.set(body, compiled);
|
|
90
|
-
return compiled;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
13
|
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
94
14
|
function guestLocation(err) {
|
|
95
15
|
const stack = String(err?.stack);
|
|
@@ -100,64 +20,8 @@ function guestLocation(err) {
|
|
|
100
20
|
return { line, col: Number(m[2]) };
|
|
101
21
|
}
|
|
102
22
|
|
|
103
|
-
const MAX_DEPTH = 64;
|
|
104
|
-
const MAX_TYPED_ARRAY = 4096;
|
|
105
|
-
|
|
106
|
-
function plainFromBinary(value) {
|
|
107
|
-
const bytes = value.byteLength;
|
|
108
|
-
if (value instanceof ArrayBuffer) value = new Uint8Array(value);
|
|
109
|
-
if (value.length > MAX_TYPED_ARRAY) return "[" + value.constructor.name + " " + bytes + " bytes]";
|
|
110
|
-
return Array.from(value, (x) => (typeof x === "bigint" ? x.toString() + "n" : x));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function plainFromMap(value, seen, depth) {
|
|
114
|
-
const allStringKeys = [...value.keys()].every((k) => typeof k === "string");
|
|
115
|
-
if (!allStringKeys) return [...value].map(([k, v]) => [toPlain(k, seen, depth + 1), toPlain(v, seen, depth + 1)]);
|
|
116
|
-
const out = {};
|
|
117
|
-
for (const [k, v] of value) out[k] = toPlain(v, seen, depth + 1);
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function plainFromCollection(value, seen, depth) {
|
|
122
|
-
if (Array.isArray(value)) return value.map((x) => toPlain(x, seen, depth + 1));
|
|
123
|
-
if (value instanceof Set) return [...value].map((x) => toPlain(x, seen, depth + 1));
|
|
124
|
-
if (value instanceof Map) return plainFromMap(value, seen, depth);
|
|
125
|
-
const out = {};
|
|
126
|
-
for (const k of Object.keys(value)) out[k] = toPlain(value[k], seen, depth + 1);
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Convert any guest value to structured-clone-safe, JSON-shaped data. */
|
|
131
|
-
function toPlain(value, seen = new Set(), depth = 0) {
|
|
132
|
-
if (value === null || value === undefined) return value;
|
|
133
|
-
const t = typeof value;
|
|
134
|
-
if (t === "string" || t === "number" || t === "boolean") return value;
|
|
135
|
-
if (t === "bigint") return value.toString() + "n";
|
|
136
|
-
if (t === "function") return "[Function" + (value.name ? " " + value.name : "") + "]";
|
|
137
|
-
if (t === "symbol") return value.toString();
|
|
138
|
-
if (depth > MAX_DEPTH) return "[Depth]";
|
|
139
|
-
if (seen.has(value)) return "[Circular]";
|
|
140
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
141
|
-
if (value instanceof RegExp) return value.toString();
|
|
142
|
-
if (value instanceof Error) {
|
|
143
|
-
const out = { name: value.name, message: value.message };
|
|
144
|
-
if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
if (value instanceof Promise) return "[Promise]";
|
|
148
|
-
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
|
|
149
|
-
if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
|
|
150
|
-
seen.add(value);
|
|
151
|
-
try {
|
|
152
|
-
return plainFromCollection(value, seen, depth);
|
|
153
|
-
} finally {
|
|
154
|
-
seen.delete(value);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---- RPC to the host thread ----
|
|
159
|
-
|
|
160
23
|
let activeRunId = 0;
|
|
24
|
+
let runActive = false;
|
|
161
25
|
let rpcSeq = 0;
|
|
162
26
|
const pendingRpc = new Map();
|
|
163
27
|
|
|
@@ -165,12 +29,13 @@ function post(msg) {
|
|
|
165
29
|
parentPort.postMessage(msg);
|
|
166
30
|
}
|
|
167
31
|
|
|
168
|
-
function
|
|
32
|
+
function callRpc(runId, method, args) {
|
|
33
|
+
if (!runActive || runId !== activeRunId) return Promise.reject(new Error("program is already complete"));
|
|
169
34
|
return new Promise((resolve, reject) => {
|
|
170
35
|
const id = ++rpcSeq;
|
|
171
36
|
pendingRpc.set(id, { resolve, reject });
|
|
172
37
|
try {
|
|
173
|
-
post({ op: "rpc", id, runId
|
|
38
|
+
post({ op: "rpc", id, runId, method, args });
|
|
174
39
|
} catch (err) {
|
|
175
40
|
pendingRpc.delete(id);
|
|
176
41
|
reject(new Error("nova." + method + " arguments are not transferable: " + err?.message));
|
|
@@ -179,6 +44,7 @@ function rpc(method, args) {
|
|
|
179
44
|
}
|
|
180
45
|
|
|
181
46
|
function unwrapValue(res) {
|
|
47
|
+
if (res?.ok === false) throw new Error(String(res.value ?? res.error ?? "host tool failed"));
|
|
182
48
|
if ("value" in Object(res)) return res.value;
|
|
183
49
|
return res;
|
|
184
50
|
}
|
|
@@ -201,7 +67,8 @@ function leanEnvelope(res) {
|
|
|
201
67
|
return res;
|
|
202
68
|
}
|
|
203
69
|
|
|
204
|
-
function buildGuestApi(available) {
|
|
70
|
+
function buildGuestApi(available, batchRead, runId) {
|
|
71
|
+
const rpc = (method, args) => callRpc(runId, method, args);
|
|
205
72
|
const availableSet = new Set(available);
|
|
206
73
|
const nova = {
|
|
207
74
|
search: (query, limit) => rpc("search", [query, limit]),
|
|
@@ -238,7 +105,9 @@ function buildGuestApi(available) {
|
|
|
238
105
|
const readArgs = (p, a, b) => (isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b });
|
|
239
106
|
const read = async (p, a, b) => {
|
|
240
107
|
if (Array.isArray(p)) {
|
|
108
|
+
if (!batchRead) return Promise.all(p.map(item => read(item, a, b)));
|
|
241
109
|
const res = await nova.call("read", readArgs(p, a, b));
|
|
110
|
+
unwrapValue(res);
|
|
242
111
|
if (Array.isArray(res?.items)) return res.items;
|
|
243
112
|
// Captured host executor without batch support: fan out.
|
|
244
113
|
return Promise.all(p.map((item) => read(item, a, b)));
|
|
@@ -277,8 +146,13 @@ function buildGuestApi(available) {
|
|
|
277
146
|
|
|
278
147
|
function makeConsole(runId, limits) {
|
|
279
148
|
let count = 0;
|
|
149
|
+
let truncated = false;
|
|
150
|
+
const markTruncated = () => {
|
|
151
|
+
if (!truncated) post({ op: "logTruncated", runId });
|
|
152
|
+
truncated = true;
|
|
153
|
+
};
|
|
280
154
|
const emit = (...args) => {
|
|
281
|
-
if (count >= limits.maxLogLines) return;
|
|
155
|
+
if (count >= limits.maxLogLines) { markTruncated(); return; }
|
|
282
156
|
count++;
|
|
283
157
|
const line = args
|
|
284
158
|
.map((a) => {
|
|
@@ -290,30 +164,34 @@ function makeConsole(runId, limits) {
|
|
|
290
164
|
}
|
|
291
165
|
})
|
|
292
166
|
.join(" ");
|
|
293
|
-
|
|
167
|
+
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
168
|
+
if (clipped.truncated) markTruncated();
|
|
169
|
+
post({ op: "log", runId, line: clipped.text, truncated: clipped.truncated });
|
|
294
170
|
};
|
|
295
171
|
return { log: emit, warn: emit, error: emit, info: emit, debug: emit };
|
|
296
172
|
}
|
|
297
173
|
|
|
298
174
|
function postFailure(runId, err, location) {
|
|
175
|
+
runActive = false;
|
|
299
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
300
177
|
post({ op: "error", runId, message, location });
|
|
301
178
|
}
|
|
302
179
|
|
|
303
180
|
async function handleRun(msg) {
|
|
304
|
-
const { runId,
|
|
181
|
+
const { runId, prepared, limits, available, batchRead = true } = msg;
|
|
305
182
|
activeRunId = runId;
|
|
183
|
+
runActive = true;
|
|
306
184
|
let compiled;
|
|
307
185
|
try {
|
|
308
|
-
compiled =
|
|
186
|
+
compiled = { fn: new AsyncFunction(...PARAMS, prepared.body), hasReturn: prepared.hasReturn };
|
|
309
187
|
} catch (err) {
|
|
310
188
|
postFailure(runId, err);
|
|
311
189
|
return;
|
|
312
190
|
}
|
|
313
|
-
const api = buildGuestApi(available);
|
|
191
|
+
const api = buildGuestApi(available, batchRead, runId);
|
|
314
192
|
const scopedConsole = makeConsole(runId, limits);
|
|
315
193
|
try {
|
|
316
|
-
const value = await compiled(
|
|
194
|
+
const value = await compiled.fn(
|
|
317
195
|
api.nova, scopedConsole, runParallel, runPipeline,
|
|
318
196
|
api.read, api.write, api.edit, api.patch, api.surface, api.snap, api.evidence, api.bash, api.exec, api.speculate,
|
|
319
197
|
);
|
|
@@ -324,7 +202,8 @@ async function handleRun(msg) {
|
|
|
324
202
|
} catch (err) {
|
|
325
203
|
plain = "[unserializable: " + (err?.message || err) + "]";
|
|
326
204
|
}
|
|
327
|
-
|
|
205
|
+
runActive = false;
|
|
206
|
+
post({ op: "done", runId, value: plain, undefinedReturn: value === undefined && !compiled.hasReturn, hasReturn: compiled.hasReturn });
|
|
328
207
|
} catch (err) {
|
|
329
208
|
if (runId !== activeRunId) return;
|
|
330
209
|
postFailure(runId, err, guestLocation(err));
|