pi-supernova 0.8.2 → 0.9.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 +188 -51
- package/docs/CHANGELOG.md +86 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +14 -30
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +6 -1
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -227
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +2 -1
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +21 -75
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +6 -5
- package/src/runtime/runtime.js +77 -253
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import {buildGuestApi} from "./guest-api.js";
|
|
2
|
+
import {packageFinalReturn} from "../output/final.js";
|
|
1
3
|
import { parentPort } from "node:worker_threads";
|
|
2
|
-
|
|
4
|
+
|
|
3
5
|
import * as nodeModule from "node:module";
|
|
4
|
-
import { isString, isObject, isFunction, toPlain
|
|
5
|
-
import { truncateChars } from "../output/format.js";
|
|
6
|
-
|
|
7
|
-
import { classifyEdit } from "../contract/edit.js";
|
|
8
|
-
import { normalizeBash } from "../contract/bash.js";
|
|
6
|
+
import { errorMessage, isString, isObject, isFunction, toPlain } from "../shared/decode.js";
|
|
7
|
+
import { truncateChars,formatReturn,displayExceeds,formatBoundedValue } from "../output/format.js";
|
|
8
|
+
|
|
9
9
|
import { guestImportMessage, isDeniedGuestImport } from "./guest-deny-imports.js";
|
|
10
10
|
|
|
11
11
|
const { register, registerHooks } = nodeModule;
|
|
12
|
+
// heapUsed/external belong to this worker; rss includes the entire Pi/OMP host.
|
|
13
|
+
const memoryUsage = process.memoryUsage.bind(process);
|
|
12
14
|
|
|
13
15
|
if (isFunction(registerHooks)) {
|
|
14
16
|
registerHooks({
|
|
@@ -41,21 +43,17 @@ const PARAMS = ["console", "read", "edit", "write", "bash"];
|
|
|
41
43
|
|
|
42
44
|
const BODY_LINE_OFFSET = 2;
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const PER_PATH_HINT = "; use Promise.allSettled(paths.map(path => read(path))) for per-path outcomes";
|
|
47
|
-
|
|
48
|
-
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
46
|
+
/** Only match the guest source, never an anonymous callback in the host bridge. */
|
|
49
47
|
function guestLocation(err) {
|
|
50
48
|
const stack = String(err?.stack);
|
|
51
|
-
const m =
|
|
49
|
+
const m = /^\s+at (async )?[^\n]*\(supernova-guest\.js:(\d+):(\d+)\)$/m.exec(stack);
|
|
52
50
|
|
|
53
51
|
if (!m) return null;
|
|
54
|
-
const line = Number(m[
|
|
52
|
+
const line = Number(m[2]) - BODY_LINE_OFFSET;
|
|
55
53
|
|
|
56
54
|
if (line < 1) return null;
|
|
57
55
|
|
|
58
|
-
return { line, col: Number(m[
|
|
56
|
+
return { line, col: Number(m[3]), awaited: Boolean(m[1]) };
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
let activeRunId = 0;
|
|
@@ -70,15 +68,23 @@ function post(msg) {
|
|
|
70
68
|
parentPort.postMessage(msg);
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
function
|
|
71
|
+
function reportMemory(runId) {
|
|
72
|
+
if (!runActive || runId !== activeRunId) return;
|
|
73
|
+
const { heapUsed, external } = memoryUsage();
|
|
74
|
+
// arrayBuffers is already included in external; do not double-charge it.
|
|
75
|
+
post({ op: "memory", runId, heapUsed, external });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function callRpc(runId, method, args, onItem) {
|
|
74
79
|
if (!runActive || runId !== activeRunId) return Promise.reject(new Error("program is already complete"));
|
|
75
80
|
|
|
76
81
|
return new Promise((resolve, reject) => {
|
|
77
82
|
const id = ++rpcSeq;
|
|
78
|
-
pendingRpc.set(id, { resolve, reject });
|
|
83
|
+
pendingRpc.set(id, { resolve, reject, onItem });
|
|
79
84
|
|
|
80
85
|
try {
|
|
81
|
-
|
|
86
|
+
reportMemory(runId);
|
|
87
|
+
post({ op: "rpc", id, runId, method, args, streamRead: Boolean(onItem) });
|
|
82
88
|
} catch (err) {
|
|
83
89
|
pendingRpc.delete(id);
|
|
84
90
|
reject(new Error("nova." + method + " arguments are not transferable: " + err?.message));
|
|
@@ -86,202 +92,25 @@ function callRpc(runId, method, args) {
|
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
function
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function unwrapRead(res, args) {
|
|
98
|
-
const value = unwrapValue(res);
|
|
99
|
-
|
|
100
|
-
if ((args.complete === true || args.json !== undefined) && res?.truncated) throw new Error("incomplete read: complete:true or json refuses truncated host output");
|
|
101
|
-
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Keep details/truncated reachable but out of the returned literal unless they carry signal. */
|
|
106
|
-
function leanEnvelope(res) {
|
|
107
|
-
if (!isObject(res)) return res;
|
|
108
|
-
|
|
109
|
-
if ("details" in res) Object.defineProperty(res, "details", { value: res.details, enumerable: false, writable: true });
|
|
110
|
-
|
|
111
|
-
for (const key of Object.keys(res)) if (res[key] === undefined) delete res[key];
|
|
112
|
-
|
|
113
|
-
if (res.truncated === false) delete res.truncated;
|
|
114
|
-
|
|
115
|
-
return res;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function swallow(promise) {
|
|
119
|
-
promise.catch(() => {});
|
|
120
|
-
|
|
121
|
-
return promise;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function throwReadPathError(target, detail) {
|
|
125
|
-
throw new Error(`read failed for ${target}: ${detail}${PER_PATH_HINT}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function isQueryUri(item) {
|
|
129
|
-
return QUERY_URI.test(item);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function firstItemErrorIndex(res) {
|
|
133
|
-
return res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function missingResolvedIndex(values, paths) {
|
|
137
|
-
return values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(paths[index]));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function rpcReadWave(rpc, wave) {
|
|
141
|
-
if (wave.length === 1) {
|
|
142
|
-
const res = leanEnvelope(await rpc("call", ["read", wave[0].args]));
|
|
143
|
-
|
|
144
|
-
return { values: [unwrapRead(res, wave[0].args)], errors: [] };
|
|
145
|
-
}
|
|
146
|
-
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
147
|
-
const res = leanEnvelope(await rpc("call", ["read", args]));
|
|
148
|
-
unwrapRead(res, args);
|
|
149
|
-
|
|
150
|
-
return { values: res.items, errors: res.itemErrors ?? [] };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function settleReadWave(wave, values, errors) {
|
|
154
|
-
if (!Array.isArray(values) || values.length !== wave.length) throw new Error("invalid batch read response");
|
|
155
|
-
|
|
156
|
-
for (let i = 0; i < wave.length; i++) {
|
|
157
|
-
if (errors[i]) wave[i].reject(new Error(errors[i]));
|
|
158
|
-
else wave[i].resolve(values[i]);
|
|
95
|
+
function encodeConsoleArg(a, limit) {
|
|
96
|
+
if (isString(a)) {
|
|
97
|
+
const raw = truncateChars(a,limit,"log");
|
|
98
|
+
const encoded = truncateChars(formatReturn(raw.text),limit,"log");
|
|
99
|
+
encoded.truncated ||= raw.truncated;
|
|
100
|
+
return encoded;
|
|
159
101
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function rejectReadWave(wave, error) {
|
|
163
|
-
for (const job of wave) job.reject(error);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function dispatchReadWaves(pending, pendingReadWaves, enqueueHost, rpc) {
|
|
167
|
-
for (let start = 0; start < pending.length; start += 64) {
|
|
168
|
-
const wave = pending.slice(start, start + 64);
|
|
169
|
-
const delivery = enqueueHost(() => rpcReadWave(rpc, wave))
|
|
170
|
-
.then(({ values, errors }) => settleReadWave(wave, values, errors))
|
|
171
|
-
.catch(error => rejectReadWave(wave, error));
|
|
172
|
-
|
|
173
|
-
pendingReadWaves.add(delivery);
|
|
174
|
-
void delivery.finally(() => pendingReadWaves.delete(delivery));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function enqueueCompatibleRead(readState, flushReads, args, decode) {
|
|
179
|
-
const key = JSON.stringify({ ...args, path: undefined });
|
|
180
|
-
|
|
181
|
-
if (readState.queued.length && readState.queued[0].key !== key) flushReads();
|
|
182
|
-
|
|
183
|
-
const promise = new Promise((resolve, reject) => {
|
|
184
|
-
readState.queued.push({ args, key, resolve, reject });
|
|
185
|
-
|
|
186
|
-
if (readState.queued.length === 1) queueMicrotask(flushReads);
|
|
187
|
-
}).then(decode);
|
|
188
|
-
|
|
189
|
-
return swallow(promise);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function readManyPaths(readOne, invoke, batchRead, args, paths, decode) {
|
|
193
|
-
assertReadPaths(paths);
|
|
194
|
-
const readEach = async () => {
|
|
195
|
-
const values = await Promise.all(paths.map(item => readOne({ ...args, path: item })));
|
|
196
|
-
const missing = args.resolve ? missingResolvedIndex(values, paths) : -1;
|
|
197
|
-
|
|
198
|
-
if (missing >= 0) throwReadPathError(paths[missing], "not_found");
|
|
199
|
-
|
|
200
|
-
return values;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const guardedReadEach = () => swallow(readEach());
|
|
204
|
-
|
|
205
|
-
if (!batchRead || args.resolve || paths.some(isQueryUri)) return guardedReadEach();
|
|
206
|
-
const res = await invoke("read", args);
|
|
207
|
-
const failed = firstItemErrorIndex(res);
|
|
208
|
-
|
|
209
|
-
if (failed >= 0) throwReadPathError(paths[failed], res.itemErrors[failed]);
|
|
210
|
-
unwrapRead(res, args);
|
|
211
|
-
|
|
212
|
-
if (Array.isArray(res?.items)) return res.items.map(decode);
|
|
213
|
-
|
|
214
|
-
return guardedReadEach();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function attachCallManyMeta(wave) {
|
|
218
|
-
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
219
|
-
Object.defineProperties(results, {
|
|
220
|
-
mode: { value: wave?.mode, enumerable: false },
|
|
221
|
-
reason: { value: wave?.reason, enumerable: false },
|
|
222
|
-
results: { value: results, enumerable: false },
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return results;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc) {
|
|
229
|
-
let began = false;
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
await drainReads();
|
|
233
|
-
await enqueueHost(() => rpc("speculateBegin", []));
|
|
234
|
-
began = true;
|
|
235
|
-
const value = await checkpointScope.run(token, fn);
|
|
236
|
-
await drainReads();
|
|
237
|
-
await enqueueHost(() => rpc("speculateCommit", []));
|
|
238
|
-
|
|
239
|
-
return { ok: true, committed: true, value };
|
|
240
|
-
} catch (err) {
|
|
241
|
-
await drainReads();
|
|
242
|
-
|
|
243
|
-
if (began) await enqueueHost(() => rpc("speculateRollback", []));
|
|
244
|
-
|
|
245
|
-
throw err;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function formatBashFailure(command, res) {
|
|
250
|
-
let exitCode;
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
exitCode = JSON.parse(res.details).exitCode;
|
|
254
|
-
} catch {}
|
|
255
|
-
|
|
256
|
-
const output = String(res.value).trimEnd();
|
|
257
|
-
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
258
|
-
|
|
259
|
-
return "command failed" + suffix + ": " + truncateChars(command, 240, "command").text + (output ? "\n" + output : "");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function markTruncatedOutput(res, text) {
|
|
263
|
-
if (res?.truncated && isString(text) && !text.includes("truncated")) return text + "\n…[output truncated]…";
|
|
264
|
-
|
|
265
|
-
return text;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function encodeConsoleArg(a) {
|
|
269
|
-
if (isString(a)) return a;
|
|
270
|
-
|
|
271
102
|
try {
|
|
272
103
|
const plain = toPlain(a);
|
|
104
|
+
if (displayExceeds(plain,limit)) return {text:formatBoundedValue(plain,limit,"log"),truncated:true};
|
|
273
105
|
const encoded = JSON.stringify(plain);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} catch {
|
|
277
|
-
return String(a);
|
|
278
|
-
}
|
|
106
|
+
return truncateChars(encoded === undefined ? String(plain) : encoded,limit,"log");
|
|
107
|
+
} catch { return truncateChars(formatReturn(String(a)),limit,"log"); }
|
|
279
108
|
}
|
|
280
109
|
|
|
281
110
|
function compileGuest(prepared, data) {
|
|
282
111
|
const bindings = data === undefined ? PARAMS : [...PARAMS, "data"];
|
|
283
112
|
|
|
284
|
-
return { fn: new AsyncFunction(...bindings, prepared.body), hasReturn: prepared.hasReturn };
|
|
113
|
+
return { fn: new AsyncFunction(...bindings, prepared.body + "\n//# sourceURL=supernova-guest.js"), hasReturn: prepared.hasReturn };
|
|
285
114
|
}
|
|
286
115
|
|
|
287
116
|
function plainGuestValue(value) {
|
|
@@ -299,129 +128,14 @@ function handleRpcResult(msg) {
|
|
|
299
128
|
else pending.reject(new Error(msg.error));
|
|
300
129
|
}
|
|
301
130
|
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
let
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
let operationTail = Promise.resolve();
|
|
314
|
-
|
|
315
|
-
const enqueueHost = operation => {
|
|
316
|
-
const next = operationTail.then(operation);
|
|
317
|
-
operationTail = next.then(() => {}, () => {});
|
|
318
|
-
|
|
319
|
-
return next;
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const nova = {
|
|
323
|
-
call(name, args) {
|
|
324
|
-
assertScope(); flushReads();
|
|
325
|
-
|
|
326
|
-
return swallow(enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope)));
|
|
327
|
-
},
|
|
328
|
-
callMany(calls) {
|
|
329
|
-
assertScope(); flushReads();
|
|
330
|
-
|
|
331
|
-
return swallow(enqueueHost(async () => attachCallManyMeta(await rpc("callMany", [calls]))));
|
|
332
|
-
},
|
|
333
|
-
async speculate(fn) {
|
|
334
|
-
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
335
|
-
const token = {};
|
|
336
|
-
checkpoint = token;
|
|
337
|
-
|
|
338
|
-
try { return await runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc); }
|
|
339
|
-
finally { checkpoint = null; }
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
// Coalesce already-started compatible reads without rewriting JS control flow.
|
|
344
|
-
const readState = { queued: [], waves: new Set() };
|
|
345
|
-
|
|
346
|
-
function flushReads() {
|
|
347
|
-
const pending = readState.queued;
|
|
348
|
-
readState.queued = [];
|
|
349
|
-
dispatchReadWaves(pending, readState.waves, enqueueHost, rpc);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function drainReads() {
|
|
353
|
-
for (;;) {
|
|
354
|
-
flushReads();
|
|
355
|
-
const waves = [...readState.waves];
|
|
356
|
-
|
|
357
|
-
if (!waves.length) return;
|
|
358
|
-
await Promise.allSettled(waves);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const invoke = (name, args) => {
|
|
363
|
-
assertScope(); flushReads();
|
|
364
|
-
|
|
365
|
-
return swallow(nova.call(name, args));
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const read = async (p, a, b) => {
|
|
369
|
-
assertScope();
|
|
370
|
-
const args = normalizeRead(gatherReadArgs(p, a, b));
|
|
371
|
-
const decode = value => decodeReadValue(args, value);
|
|
372
|
-
p = args.path;
|
|
373
|
-
|
|
374
|
-
if (Array.isArray(p)) {
|
|
375
|
-
const values = await readManyPaths(read, invoke, batchRead, args, p, decode);
|
|
376
|
-
for (const item of p) if (isString(item)) noteReadPath({ path: item }, values);
|
|
377
|
-
|
|
378
|
-
return values;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const readValue = !batchRead
|
|
382
|
-
? decode(unwrapRead(await invoke("read", args), args))
|
|
383
|
-
: await enqueueCompatibleRead(readState, flushReads, args, decode);
|
|
384
|
-
noteReadPath(args, readValue);
|
|
385
|
-
|
|
386
|
-
return readValue;
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const readFiles = new Set();
|
|
390
|
-
|
|
391
|
-
function noteReadPath(args, value) {
|
|
392
|
-
if (isString(args.path) && looksLikePath(args.path)) readFiles.add(args.path);
|
|
393
|
-
if (isObject(value) && isString(value.path) && looksLikePath(value.path)) readFiles.add(value.path);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const write = async (p, content) => {
|
|
397
|
-
const args = isObject(p) ? p : { path: p, content };
|
|
398
|
-
|
|
399
|
-
if (args.append !== true && args.replace !== true && isString(args.path) && readFiles.has(args.path)) {
|
|
400
|
-
throw new Error("file was already read this program; use edit(oldText, newText) or edit(view, ...). write({path,content,replace:true}) replaces anyway");
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return unwrapValue(await invoke("write", args));
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const edit = async (p, oldText, newText) => {
|
|
407
|
-
const classified = classifyEdit(p, oldText, newText);
|
|
408
|
-
|
|
409
|
-
if (classified.kind === "checkpoint") return nova.speculate(classified.fn);
|
|
410
|
-
|
|
411
|
-
return unwrapValue(await invoke(classified.command, classified.args));
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const bash = async (command, opts) => {
|
|
415
|
-
const args = normalizeBash(command, opts);
|
|
416
|
-
command = args.command;
|
|
417
|
-
const res = await invoke("bash", args);
|
|
418
|
-
|
|
419
|
-
if (res?.ok === false) throw new Error(formatBashFailure(command, res));
|
|
420
|
-
|
|
421
|
-
return markTruncatedOutput(res, unwrapValue(res));
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
return { read, write, edit, bash, nova };
|
|
131
|
+
function handleReadItem(msg) {
|
|
132
|
+
const pending = pendingRpc.get(msg.id);
|
|
133
|
+
if (!pending?.onItem || !runActive || msg.runId !== activeRunId) return;
|
|
134
|
+
let error;
|
|
135
|
+
try { pending.onItem(msg.index,msg.value); }
|
|
136
|
+
catch (err) { error = errorMessage(err); pending.reject(err); pendingRpc.delete(msg.id); }
|
|
137
|
+
reportMemory(activeRunId);
|
|
138
|
+
post({op:"rpc:ack",id:msg.id,index:msg.index,runId:activeRunId,error});
|
|
425
139
|
}
|
|
426
140
|
|
|
427
141
|
function makeConsole(runId, limits) {
|
|
@@ -439,7 +153,15 @@ function makeConsole(runId, limits) {
|
|
|
439
153
|
return; }
|
|
440
154
|
|
|
441
155
|
count++;
|
|
442
|
-
|
|
156
|
+
let line = "", first = true;
|
|
157
|
+
for (const arg of args) {
|
|
158
|
+
const room = limits.maxLogLineChars-line.length-(first ? 0 : 1);
|
|
159
|
+
if (room <= 0) { markTruncated(); break; }
|
|
160
|
+
const encoded = encodeConsoleArg(arg,room);
|
|
161
|
+
if (encoded.truncated) markTruncated();
|
|
162
|
+
line += (first ? "" : " ")+encoded.text;
|
|
163
|
+
first = false;
|
|
164
|
+
}
|
|
443
165
|
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
444
166
|
|
|
445
167
|
if (clipped.truncated) markTruncated();
|
|
@@ -451,7 +173,7 @@ function makeConsole(runId, limits) {
|
|
|
451
173
|
|
|
452
174
|
function postFailure(runId, err, location) {
|
|
453
175
|
runActive = false;
|
|
454
|
-
const message =
|
|
176
|
+
const message = errorMessage(err);
|
|
455
177
|
post({ op: "error", runId, message, location });
|
|
456
178
|
}
|
|
457
179
|
|
|
@@ -495,7 +217,7 @@ function sealGuestRealm() {
|
|
|
495
217
|
}
|
|
496
218
|
|
|
497
219
|
async function handleRun(msg) {
|
|
498
|
-
const { runId, prepared, limits,
|
|
220
|
+
const { runId, prepared, limits, batchRead = true } = msg;
|
|
499
221
|
activeRunId = runId;
|
|
500
222
|
runActive = true;
|
|
501
223
|
let compiled;
|
|
@@ -510,8 +232,9 @@ async function handleRun(msg) {
|
|
|
510
232
|
return;
|
|
511
233
|
}
|
|
512
234
|
|
|
513
|
-
const api = buildGuestApi(
|
|
235
|
+
const api = buildGuestApi((method, args, onItem) => callRpc(runId, method, args, onItem), batchRead);
|
|
514
236
|
const scopedConsole = makeConsole(runId, limits);
|
|
237
|
+
const memoryTimer = setInterval(() => reportMemory(runId), 50);
|
|
515
238
|
|
|
516
239
|
try {
|
|
517
240
|
const value = await compiled.fn(
|
|
@@ -520,17 +243,23 @@ async function handleRun(msg) {
|
|
|
520
243
|
|
|
521
244
|
if (runId !== activeRunId) return;
|
|
522
245
|
const plain = plainGuestValue(value);
|
|
246
|
+
// Reduce model output while it is still covered by worker memory/deadlines.
|
|
247
|
+
// Only a bounded display and retained images cross back to the Pi/OMP host.
|
|
248
|
+
const result = msg.formatResult ? {output:packageFinalReturn(plain,[],limits)} : {value:plain};
|
|
249
|
+
reportMemory(runId);
|
|
523
250
|
runActive = false;
|
|
524
|
-
post({ op:
|
|
251
|
+
post({ op:"done",runId,...result,undefinedReturn:value === undefined && !compiled.hasReturn,hasReturn:compiled.hasReturn });
|
|
525
252
|
} catch (err) {
|
|
526
253
|
if (runId !== activeRunId) return;
|
|
527
254
|
postFailure(runId, err, guestLocation(err));
|
|
528
|
-
}
|
|
255
|
+
} finally { clearInterval(memoryTimer); }
|
|
529
256
|
}
|
|
530
257
|
|
|
531
258
|
parentPort.on("message", (msg) => {
|
|
532
259
|
if (!isObject(msg)) return;
|
|
533
260
|
|
|
261
|
+
if (msg.op === "rpc:item") { handleReadItem(msg); return; }
|
|
262
|
+
|
|
534
263
|
if (msg.op === "rpc:result") {
|
|
535
264
|
handleRpcResult(msg);
|
|
536
265
|
|
package/src/runtime/parallel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFunction, isObject } from "../shared/decode.js";
|
|
1
|
+
import { isFunction, isObject, isString } from "../shared/decode.js";
|
|
2
2
|
|
|
3
3
|
const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "evidence", "surface", "asgrep_search", "asgrep_status", "ast_grep", "web_search"]);
|
|
4
4
|
|
|
@@ -47,18 +47,18 @@ export function isMutatingTool(name, config = {}, args = {}, definition) {
|
|
|
47
47
|
return !READ_ONLY_TOOLS.has(name);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
while (wave.length < maxParallelReads && queue[0]?.name === "read") {
|
|
56
|
-
const next = queue.shift();
|
|
57
|
-
|
|
58
|
-
if (!next.cancelled) wave.push(next);
|
|
59
|
-
}
|
|
50
|
+
function compatibleScheduledJob(job, active) {
|
|
51
|
+
return [...active].every(running =>
|
|
52
|
+
(job.name === "read" && running.name === "read") ||
|
|
53
|
+
(isString(job.key) && isString(running.key) && job.key !== running.key));
|
|
54
|
+
}
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
async function prepareScheduledJob(job) {
|
|
57
|
+
if (!job.resolveKey) return;
|
|
58
|
+
// Key lookup is advisory. Invalid paths still run in isolation so the real
|
|
59
|
+
// adapter reports the error through its normal trace/permission checks.
|
|
60
|
+
try { job.key = await job.resolveKey(); } catch {}
|
|
61
|
+
job.resolveKey = undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async function runScheduledJob(job) {
|
|
@@ -74,12 +74,6 @@ async function runScheduledJob(job) {
|
|
|
74
74
|
} catch (error) { job.reject(error); }
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function runScheduledWave(wave) {
|
|
78
|
-
// Settle each promise separately: one failed read must not discard its
|
|
79
|
-
// siblings or release a write barrier while other reads are still active.
|
|
80
|
-
return Promise.all(wave.map(runScheduledJob));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
77
|
function attachJobAbort(job, signal, reject) {
|
|
84
78
|
job.abort = () => {
|
|
85
79
|
if (job.started) return;
|
|
@@ -91,34 +85,47 @@ function attachJobAbort(job, signal, reject) {
|
|
|
91
85
|
signal?.addEventListener("abort", job.abort, { once: true });
|
|
92
86
|
}
|
|
93
87
|
|
|
94
|
-
/** FIFO
|
|
88
|
+
/** FIFO admission: concurrent reads or disjoint native files, otherwise a barrier. */
|
|
95
89
|
export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
96
90
|
if (!Number.isInteger(maxParallelReads) || maxParallelReads < 1) throw new Error("maxParallelReads must be a positive integer");
|
|
97
91
|
const queue = [];
|
|
92
|
+
const active = new Set();
|
|
98
93
|
const stats = { calls: 0, readWaves: 0, peakParallelReads: 0 };
|
|
99
94
|
let draining = false;
|
|
100
95
|
|
|
96
|
+
function start(job) {
|
|
97
|
+
if (job.name === "read") {
|
|
98
|
+
if (!active.size) stats.readWaves++;
|
|
99
|
+
stats.peakParallelReads = Math.max(stats.peakParallelReads, active.size + 1);
|
|
100
|
+
}
|
|
101
|
+
active.add(job);
|
|
102
|
+
// Each promise settles independently. Barriers still wait for every active
|
|
103
|
+
// sibling, including when Promise.all in the guest has already rejected.
|
|
104
|
+
void runScheduledJob(job).finally(() => { active.delete(job); void drain(); });
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
async function drain() {
|
|
108
|
+
if (draining) return;
|
|
109
|
+
draining = true;
|
|
102
110
|
try {
|
|
103
|
-
while (queue.length) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await runScheduledWave(wave);
|
|
111
|
+
while (queue.length && active.size < maxParallelReads) {
|
|
112
|
+
const job = queue[0];
|
|
113
|
+
if (job.cancelled) { queue.shift(); continue; }
|
|
114
|
+
// Resolve identities after shell/override barriers, which may change
|
|
115
|
+
// symlinks. Check synchronously: a finishing job must not lose its wakeup.
|
|
116
|
+
if (job.resolveKey && [...active].some(running => !isString(running.key))) break;
|
|
117
|
+
await prepareScheduledJob(job);
|
|
118
|
+
if (job.cancelled) continue;
|
|
119
|
+
if (!compatibleScheduledJob(job, active)) break;
|
|
120
|
+
queue.shift();
|
|
121
|
+
start(job);
|
|
115
122
|
}
|
|
116
123
|
} finally { draining = false; }
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
return {
|
|
120
127
|
stats,
|
|
121
|
-
schedule(name, run, signal) {
|
|
128
|
+
schedule(name, run, signal, resolveKey) {
|
|
122
129
|
if (!NATIVE_TOOLS.includes(name) || !isFunction(run)) {
|
|
123
130
|
return Promise.reject(new Error("scheduler requires a native tool and an executor"));
|
|
124
131
|
}
|
|
@@ -127,16 +134,11 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
127
134
|
stats.calls++;
|
|
128
135
|
|
|
129
136
|
return new Promise((resolve, reject) => {
|
|
130
|
-
const job = { name, run, signal, resolve, reject, started: false, cancelled: false };
|
|
137
|
+
const job = { name, run, signal, resolve, reject, resolveKey, started: false, cancelled: false };
|
|
131
138
|
attachJobAbort(job, signal, reject);
|
|
132
139
|
queue.push(job);
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
draining = true;
|
|
136
|
-
// Pi submits sibling tools in the same turn without model-side code.
|
|
137
|
-
// Collect those submissions before selecting the first read wave.
|
|
138
|
-
queueMicrotask(() => { void drain(); });
|
|
139
|
-
}
|
|
141
|
+
void drain();
|
|
140
142
|
});
|
|
141
143
|
},
|
|
142
144
|
};
|