pi-supernova 0.6.0 → 0.7.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 +27 -3
- package/docs/CHANGELOG.md +104 -0
- package/docs/TOKEN_COSTS.md +13 -5
- package/index.js +120 -79
- package/package.json +1 -1
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +28 -222
- package/src/bridge/host-bridge.js +113 -1668
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -198
- package/src/context/evidence.js +140 -76
- package/src/context/fuzzy.js +42 -24
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +23 -18
- package/src/context/repo-index.js +206 -170
- package/src/context/search.js +157 -77
- package/src/context/snap.js +240 -136
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +14 -5
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +12 -8
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +94 -50
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +205 -175
- package/src/fs/workspace.js +119 -108
- package/src/output/bottleneck.js +195 -116
- package/src/output/format.js +101 -67
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +289 -292
- package/src/runtime/parallel.js +97 -64
- package/src/runtime/program-batch.js +178 -69
- package/src/runtime/reference.js +15 -14
- package/src/runtime/runtime.js +327 -187
- package/src/shared/decode.js +58 -36
- package/src/ui/omp-frame.js +59 -42
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +241 -145
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import {
|
|
3
|
+
import { register, registerHooks } from "node:module";
|
|
4
|
+
import { isString, isObject, isFunction, toPlain, looksLikePath } from "../shared/decode.js";
|
|
4
5
|
import { truncateChars } from "../output/format.js";
|
|
5
|
-
import {
|
|
6
|
+
import { gatherReadArgs, normalizeRead, decodeReadValue, assertReadPaths } from "../contract/read.js";
|
|
7
|
+
import { classifyEdit } from "../contract/edit.js";
|
|
8
|
+
import { normalizeBash } from "../contract/bash.js";
|
|
9
|
+
import { guestImportMessage, isDeniedGuestImport } from "./guest-deny-imports.js";
|
|
10
|
+
|
|
11
|
+
if (isFunction(registerHooks)) {
|
|
12
|
+
registerHooks({
|
|
13
|
+
resolve(specifier, context, nextResolve) {
|
|
14
|
+
if (isDeniedGuestImport(specifier)) {
|
|
15
|
+
const error = new Error(guestImportMessage(specifier));
|
|
16
|
+
error.code = "ERR_GUEST_IMPORT";
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return nextResolve(specifier, context);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
register("./guest-deny-imports.js", import.meta.url);
|
|
25
|
+
}
|
|
6
26
|
|
|
7
27
|
// Guest programs run here, off the host thread. The host can terminate() this
|
|
8
28
|
// worker mid-loop, so a runaway "while (true) {}" or process.exit() in guest
|
|
@@ -14,6 +34,10 @@ const PARAMS = ["console", "read", "edit", "write", "bash"];
|
|
|
14
34
|
|
|
15
35
|
const BODY_LINE_OFFSET = 2;
|
|
16
36
|
|
|
37
|
+
const QUERY_URI = /^(agent|artifact):\/\/.*\?/i;
|
|
38
|
+
|
|
39
|
+
const PER_PATH_HINT = "; use Promise.allSettled(paths.map(path => read(path))) for per-path outcomes";
|
|
40
|
+
|
|
17
41
|
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
18
42
|
function guestLocation(err) {
|
|
19
43
|
const stack = String(err?.stack);
|
|
@@ -71,16 +95,6 @@ function unwrapRead(res, args) {
|
|
|
71
95
|
return value;
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
function unwrapJsonValue(res) {
|
|
75
|
-
const value = unwrapValue(res);
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(value);
|
|
79
|
-
} catch {
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
98
|
/** Keep details/truncated reachable but out of the returned literal unless they carry signal. */
|
|
85
99
|
function leanEnvelope(res) {
|
|
86
100
|
if (!isObject(res)) return res;
|
|
@@ -94,338 +108,310 @@ function leanEnvelope(res) {
|
|
|
94
108
|
return res;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
102
|
-
const rpc = (method, args) => callRpc(runId, method, args);
|
|
103
|
-
const availableSet = new Set(available);
|
|
104
|
-
const checkpointScope = new AsyncLocalStorage();
|
|
105
|
-
let checkpoint = null;
|
|
106
|
-
|
|
107
|
-
const assertScope = () => {
|
|
108
|
-
const token = checkpointScope.getStore();
|
|
109
|
-
|
|
110
|
-
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
let operationTail = Promise.resolve();
|
|
111
|
+
function swallow(promise) {
|
|
112
|
+
promise.catch(() => {});
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
operationTail = next.then(() => {}, () => {});
|
|
114
|
+
return promise;
|
|
115
|
+
}
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
};
|
|
117
|
+
function throwReadPathError(target, detail) {
|
|
118
|
+
throw new Error(`read failed for ${target}: ${detail}${PER_PATH_HINT}`);
|
|
119
|
+
}
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const promise = enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope));
|
|
121
|
+
function isQueryUri(item) {
|
|
122
|
+
return QUERY_URI.test(item);
|
|
123
|
+
}
|
|
126
124
|
|
|
127
|
-
|
|
125
|
+
function firstItemErrorIndex(res) {
|
|
126
|
+
return res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
127
|
+
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
assertScope(); flushReads();
|
|
133
|
-
const promise = enqueueHost(async () => {
|
|
134
|
-
const wave = await rpc("callMany", [calls]);
|
|
135
|
-
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
136
|
-
Object.defineProperties(results, {
|
|
137
|
-
mode: { value: wave?.mode, enumerable: false },
|
|
138
|
-
reason: { value: wave?.reason, enumerable: false },
|
|
139
|
-
results: { value: results, enumerable: false },
|
|
140
|
-
});
|
|
129
|
+
function missingResolvedIndex(values, paths) {
|
|
130
|
+
return values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(paths[index]));
|
|
131
|
+
}
|
|
141
132
|
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
async function rpcReadWave(rpc, wave) {
|
|
134
|
+
if (wave.length === 1) {
|
|
135
|
+
const res = leanEnvelope(await rpc("call", ["read", wave[0].args]));
|
|
144
136
|
|
|
145
|
-
|
|
137
|
+
return { values: [unwrapRead(res, wave[0].args)], errors: [] };
|
|
138
|
+
}
|
|
139
|
+
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
140
|
+
const res = leanEnvelope(await rpc("call", ["read", args]));
|
|
141
|
+
unwrapRead(res, args);
|
|
146
142
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async speculate(fn) {
|
|
150
|
-
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
151
|
-
const token = {};
|
|
152
|
-
checkpoint = token;
|
|
153
|
-
let began = false;
|
|
143
|
+
return { values: res.items, errors: res.itemErrors ?? [] };
|
|
144
|
+
}
|
|
154
145
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await enqueueHost(() => rpc("speculateBegin", []));
|
|
158
|
-
began = true;
|
|
159
|
-
const value = await checkpointScope.run(token, fn);
|
|
160
|
-
await drainReads();
|
|
161
|
-
await enqueueHost(() => rpc("speculateCommit", []));
|
|
146
|
+
function settleReadWave(wave, values, errors) {
|
|
147
|
+
if (!Array.isArray(values) || values.length !== wave.length) throw new Error("invalid batch read response");
|
|
162
148
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
149
|
+
for (let i = 0; i < wave.length; i++) {
|
|
150
|
+
if (errors[i]) wave[i].reject(new Error(errors[i]));
|
|
151
|
+
else wave[i].resolve(values[i]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
166
154
|
|
|
167
|
-
|
|
155
|
+
function rejectReadWave(wave, error) {
|
|
156
|
+
for (const job of wave) job.reject(error);
|
|
157
|
+
}
|
|
168
158
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
has: (name) => availableSet.has(name),
|
|
176
|
-
};
|
|
159
|
+
function dispatchReadWaves(pending, pendingReadWaves, enqueueHost, rpc) {
|
|
160
|
+
for (let start = 0; start < pending.length; start += 64) {
|
|
161
|
+
const wave = pending.slice(start, start + 64);
|
|
162
|
+
const delivery = enqueueHost(() => rpcReadWave(rpc, wave))
|
|
163
|
+
.then(({ values, errors }) => settleReadWave(wave, values, errors))
|
|
164
|
+
.catch(error => rejectReadWave(wave, error));
|
|
177
165
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
pendingReadWaves.add(delivery);
|
|
167
|
+
void delivery.finally(() => pendingReadWaves.delete(delivery));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
181
170
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
queuedReads = [];
|
|
171
|
+
function enqueueCompatibleRead(readState, flushReads, args, decode) {
|
|
172
|
+
const key = JSON.stringify({ ...args, path: undefined });
|
|
185
173
|
|
|
186
|
-
|
|
187
|
-
const wave = pending.slice(start, start + 64);
|
|
188
|
-
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
174
|
+
if (readState.queued.length && readState.queued[0].key !== key) flushReads();
|
|
189
175
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: rpc("call", ["read", args]).then(leanEnvelope).then(res => { unwrapRead(res, args);
|
|
176
|
+
const promise = new Promise((resolve, reject) => {
|
|
177
|
+
readState.queued.push({ args, key, resolve, reject });
|
|
193
178
|
|
|
194
|
-
|
|
179
|
+
if (readState.queued.length === 1) queueMicrotask(flushReads);
|
|
180
|
+
}).then(decode);
|
|
195
181
|
|
|
196
|
-
|
|
197
|
-
|
|
182
|
+
return swallow(promise);
|
|
183
|
+
}
|
|
198
184
|
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
async function readManyPaths(readOne, invoke, batchRead, args, paths, decode) {
|
|
186
|
+
assertReadPaths(paths);
|
|
187
|
+
const readEach = async () => {
|
|
188
|
+
const values = await Promise.all(paths.map(item => readOne({ ...args, path: item })));
|
|
189
|
+
const missing = args.resolve ? missingResolvedIndex(values, paths) : -1;
|
|
201
190
|
|
|
202
|
-
|
|
203
|
-
else wave[i].resolve(value);
|
|
204
|
-
}
|
|
205
|
-
}).catch(error => { for (const job of wave) job.reject(error); });
|
|
191
|
+
if (missing >= 0) throwReadPathError(paths[missing], "not_found");
|
|
206
192
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
}
|
|
193
|
+
return values;
|
|
194
|
+
};
|
|
211
195
|
|
|
212
|
-
|
|
213
|
-
for (;;) {
|
|
214
|
-
flushReads();
|
|
215
|
-
const waves = [...pendingReadWaves];
|
|
196
|
+
const guardedReadEach = () => swallow(readEach());
|
|
216
197
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
198
|
+
if (!batchRead || args.resolve || paths.some(isQueryUri)) return guardedReadEach();
|
|
199
|
+
const res = await invoke("read", args);
|
|
200
|
+
const failed = firstItemErrorIndex(res);
|
|
221
201
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const promise = nova.call(name, args);
|
|
202
|
+
if (failed >= 0) throwReadPathError(paths[failed], res.itemErrors[failed]);
|
|
203
|
+
unwrapRead(res, args);
|
|
225
204
|
|
|
226
|
-
|
|
205
|
+
if (Array.isArray(res?.items)) return res.items.map(decode);
|
|
227
206
|
|
|
228
|
-
|
|
229
|
-
|
|
207
|
+
return guardedReadEach();
|
|
208
|
+
}
|
|
230
209
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
210
|
+
function attachCallManyMeta(wave) {
|
|
211
|
+
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
212
|
+
Object.defineProperties(results, {
|
|
213
|
+
mode: { value: wave?.mode, enumerable: false },
|
|
214
|
+
reason: { value: wave?.reason, enumerable: false },
|
|
215
|
+
results: { value: results, enumerable: false },
|
|
216
|
+
});
|
|
234
217
|
|
|
235
|
-
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
236
220
|
|
|
237
|
-
|
|
238
|
-
|
|
221
|
+
async function runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc) {
|
|
222
|
+
let began = false;
|
|
239
223
|
|
|
240
|
-
|
|
241
|
-
|
|
224
|
+
try {
|
|
225
|
+
await drainReads();
|
|
226
|
+
await enqueueHost(() => rpc("speculateBegin", []));
|
|
227
|
+
began = true;
|
|
228
|
+
const value = await checkpointScope.run(token, fn);
|
|
229
|
+
await drainReads();
|
|
230
|
+
await enqueueHost(() => rpc("speculateCommit", []));
|
|
231
|
+
|
|
232
|
+
return { ok: true, committed: true, value };
|
|
233
|
+
} catch (err) {
|
|
234
|
+
await drainReads();
|
|
242
235
|
|
|
243
|
-
|
|
244
|
-
assertScope();
|
|
245
|
-
const args = sessionJsonArgs(readArgs(p, a, b));
|
|
246
|
-
if (isString(args.path) && args.resolve === undefined && args.complete !== true && args.json === undefined && args.about === undefined && args.query === undefined && !looksLikePath(args.path) && !/^(?:agent|artifact):\/\//i.test(args.path)) {
|
|
247
|
-
args.resolve = true;
|
|
248
|
-
}
|
|
249
|
-
validateJsonRead(args);
|
|
236
|
+
if (began) await enqueueHost(() => rpc("speculateRollback", []));
|
|
250
237
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
238
|
+
return { ok: false, committed: false, error: err instanceof Error ? err.message : String(err) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
254
241
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const focusModes = [args.about !== undefined, args.query !== undefined, args.outline === true].filter(Boolean).length;
|
|
242
|
+
function formatBashFailure(command, res) {
|
|
243
|
+
let exitCode;
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if ((!args.resolve && args.json === undefined) || !isString(value)) return value;
|
|
245
|
+
try {
|
|
246
|
+
exitCode = JSON.parse(res.details).exitCode;
|
|
247
|
+
} catch {}
|
|
263
248
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
const selector = args.json === undefined ? "" : " (" + (Array.isArray(args.json) ? args.json.join(", ") : String(args.json)) + ")";
|
|
268
|
-
throw new Error("JSON read failed for " + String(args.path ?? args.target ?? "resource") + selector + ": " + (error instanceof Error ? error.message : String(error)));
|
|
269
|
-
}
|
|
270
|
-
};
|
|
249
|
+
const output = String(res.value).trimEnd();
|
|
250
|
+
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
271
251
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
p = args.path;
|
|
252
|
+
return "command failed" + suffix + ": " + command + (output ? "\n" + output : "");
|
|
253
|
+
}
|
|
275
254
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return unwrapJsonValue(await invoke("evidence", { ...args, path: evidencePath, query }));
|
|
279
|
-
}
|
|
255
|
+
function markTruncatedOutput(res, text) {
|
|
256
|
+
if (res?.truncated && isString(text) && !text.includes("truncated")) return text + "\n…[output truncated]…";
|
|
280
257
|
|
|
281
|
-
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
282
260
|
|
|
283
|
-
|
|
284
|
-
|
|
261
|
+
function encodeConsoleArg(a) {
|
|
262
|
+
if (isString(a)) return a;
|
|
285
263
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const missing = args.resolve ? values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(p[index])) : -1;
|
|
264
|
+
try {
|
|
265
|
+
const plain = toPlain(a);
|
|
266
|
+
const encoded = JSON.stringify(plain);
|
|
290
267
|
|
|
291
|
-
|
|
268
|
+
return encoded === undefined ? String(plain) : encoded;
|
|
269
|
+
} catch {
|
|
270
|
+
return String(a);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
292
273
|
|
|
293
|
-
|
|
294
|
-
|
|
274
|
+
function compileGuest(prepared, data) {
|
|
275
|
+
const bindings = data === undefined ? PARAMS : [...PARAMS, "data"];
|
|
295
276
|
|
|
296
|
-
|
|
297
|
-
|
|
277
|
+
return { fn: new AsyncFunction(...bindings, prepared.body), hasReturn: prepared.hasReturn };
|
|
278
|
+
}
|
|
298
279
|
|
|
299
|
-
|
|
280
|
+
function plainGuestValue(value) {
|
|
281
|
+
try { return toPlain(value); }
|
|
282
|
+
catch (err) { return "[unserializable: " + (err?.message || err) + "]"; }
|
|
283
|
+
}
|
|
300
284
|
|
|
301
|
-
|
|
302
|
-
|
|
285
|
+
function handleRpcResult(msg) {
|
|
286
|
+
const pending = pendingRpc.get(msg.id);
|
|
303
287
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const failed = res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
288
|
+
if (!pending) return;
|
|
289
|
+
pendingRpc.delete(msg.id);
|
|
307
290
|
|
|
308
|
-
|
|
309
|
-
|
|
291
|
+
if (msg.ok) pending.resolve(msg.value);
|
|
292
|
+
else pending.reject(new Error(msg.error));
|
|
293
|
+
}
|
|
310
294
|
|
|
311
|
-
|
|
295
|
+
function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
296
|
+
const rpc = (method, args) => callRpc(runId, method, args);
|
|
297
|
+
const checkpointScope = new AsyncLocalStorage();
|
|
298
|
+
let checkpoint = null;
|
|
312
299
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
300
|
+
const assertScope = () => {
|
|
301
|
+
const token = checkpointScope.getStore();
|
|
316
302
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
unwrapRead(res, args);
|
|
303
|
+
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
304
|
+
};
|
|
320
305
|
|
|
321
|
-
|
|
322
|
-
}
|
|
306
|
+
let operationTail = Promise.resolve();
|
|
323
307
|
|
|
324
|
-
|
|
308
|
+
const enqueueHost = operation => {
|
|
309
|
+
const next = operationTail.then(operation);
|
|
310
|
+
operationTail = next.then(() => {}, () => {});
|
|
325
311
|
|
|
326
|
-
|
|
312
|
+
return next;
|
|
313
|
+
};
|
|
327
314
|
|
|
328
|
-
|
|
329
|
-
|
|
315
|
+
const nova = {
|
|
316
|
+
call(name, args) {
|
|
317
|
+
assertScope(); flushReads();
|
|
330
318
|
|
|
331
|
-
|
|
332
|
-
}
|
|
319
|
+
return swallow(enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope)));
|
|
320
|
+
},
|
|
321
|
+
callMany(calls) {
|
|
322
|
+
assertScope(); flushReads();
|
|
333
323
|
|
|
334
|
-
|
|
324
|
+
return swallow(enqueueHost(async () => attachCallManyMeta(await rpc("callMany", [calls]))));
|
|
325
|
+
},
|
|
326
|
+
async speculate(fn) {
|
|
327
|
+
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
328
|
+
const token = {};
|
|
329
|
+
checkpoint = token;
|
|
335
330
|
|
|
336
|
-
|
|
331
|
+
try { return await runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc); }
|
|
332
|
+
finally { checkpoint = null; }
|
|
333
|
+
},
|
|
337
334
|
};
|
|
338
335
|
|
|
339
|
-
|
|
336
|
+
// Coalesce already-started compatible reads without rewriting JS control flow.
|
|
337
|
+
const readState = { queued: [], waves: new Set() };
|
|
340
338
|
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
339
|
+
function flushReads() {
|
|
340
|
+
const pending = readState.queued;
|
|
341
|
+
readState.queued = [];
|
|
342
|
+
dispatchReadWaves(pending, readState.waves, enqueueHost, rpc);
|
|
343
|
+
}
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
async function drainReads() {
|
|
346
|
+
for (;;) {
|
|
347
|
+
flushReads();
|
|
348
|
+
const waves = [...readState.waves];
|
|
346
349
|
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
if (!waves.length) return;
|
|
351
|
+
await Promise.allSettled(waves);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
349
354
|
|
|
350
|
-
const
|
|
355
|
+
const invoke = (name, args) => {
|
|
356
|
+
assertScope(); flushReads();
|
|
351
357
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const usage = 'invalid edit signature; use edit(path,oldText,newText), edit({path,edits:[{oldText,newText}]}), or edit({path,patch:"@@ -1 +1 @@\n-old\n+new\n"})';
|
|
358
|
+
return swallow(nova.call(name, args));
|
|
359
|
+
};
|
|
355
360
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
361
|
+
const read = async (p, a, b) => {
|
|
362
|
+
assertScope();
|
|
363
|
+
const args = normalizeRead(gatherReadArgs(p, a, b));
|
|
364
|
+
const decode = value => decodeReadValue(args, value);
|
|
365
|
+
p = args.path;
|
|
360
366
|
|
|
361
|
-
|
|
367
|
+
if (Array.isArray(p)) {
|
|
368
|
+
const values = await readManyPaths(read, invoke, batchRead, args, p, decode);
|
|
369
|
+
for (const item of p) if (isString(item)) noteReadPath({ path: item }, values);
|
|
362
370
|
|
|
363
|
-
return
|
|
371
|
+
return values;
|
|
364
372
|
}
|
|
365
373
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
374
|
+
const readValue = !batchRead
|
|
375
|
+
? decode(unwrapRead(await invoke("read", args), args))
|
|
376
|
+
: await enqueueCompatibleRead(readState, flushReads, args, decode);
|
|
377
|
+
noteReadPath(args, readValue);
|
|
370
378
|
|
|
371
|
-
|
|
372
|
-
|
|
379
|
+
return readValue;
|
|
380
|
+
};
|
|
373
381
|
|
|
374
|
-
|
|
382
|
+
const readFiles = new Set();
|
|
375
383
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
384
|
+
function noteReadPath(args, value) {
|
|
385
|
+
if (isString(args.path) && looksLikePath(args.path)) readFiles.add(args.path);
|
|
386
|
+
if (isObject(value) && isString(value.path) && looksLikePath(value.path)) readFiles.add(value.path);
|
|
387
|
+
}
|
|
380
388
|
|
|
381
|
-
|
|
389
|
+
const write = async (p, content) => {
|
|
390
|
+
const args = isObject(p) ? p : { path: p, content };
|
|
382
391
|
|
|
383
|
-
|
|
392
|
+
if (args.append !== true && args.replace !== true && isString(args.path) && readFiles.has(args.path)) {
|
|
393
|
+
throw new Error("file was already read this program; use edit(oldText, newText) or edit(view, ...). write({path,content,replace:true}) replaces anyway");
|
|
384
394
|
}
|
|
385
395
|
|
|
386
|
-
return unwrapValue(await invoke(
|
|
396
|
+
return unwrapValue(await invoke("write", args));
|
|
387
397
|
};
|
|
388
398
|
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
if (args.args !== undefined) {
|
|
393
|
-
if (!isString(args.command) || !Array.isArray(args.args)) throw new Error("bash argv requires a command string and an array of string args");
|
|
399
|
+
const edit = async (p, oldText, newText) => {
|
|
400
|
+
const classified = classifyEdit(p, oldText, newText);
|
|
394
401
|
|
|
395
|
-
|
|
396
|
-
args.args = args.args.map(String);
|
|
402
|
+
if (classified.kind === "checkpoint") return nova.speculate(classified.fn);
|
|
397
403
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (process.platform === "win32") {
|
|
401
|
-
delete args._directArgv;
|
|
402
|
-
args.command = [args.command, ...args.args].map(quoteShellArg).join(" ");
|
|
403
|
-
delete args.args;
|
|
404
|
-
} else args._directArgv = true;
|
|
405
|
-
}
|
|
404
|
+
return unwrapValue(await invoke(classified.command, classified.args));
|
|
405
|
+
};
|
|
406
406
|
|
|
407
|
+
const bash = async (command, opts) => {
|
|
408
|
+
const args = normalizeBash(command, opts);
|
|
407
409
|
command = args.command;
|
|
408
|
-
|
|
409
|
-
if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
|
|
410
410
|
const res = await invoke("bash", args);
|
|
411
411
|
|
|
412
|
-
if (res?.ok === false)
|
|
413
|
-
let exitCode;
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
exitCode = JSON.parse(res.details).exitCode;
|
|
417
|
-
} catch {}
|
|
418
|
-
|
|
419
|
-
const output = String(res.value).trimEnd();
|
|
420
|
-
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
421
|
-
throw new Error("command failed" + suffix + ": " + command + (output ? "\n" + output : ""));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let text = unwrapValue(res);
|
|
425
|
-
|
|
426
|
-
if (res?.truncated && isString(text) && !text.includes("truncated")) text += "\n…[output truncated]…";
|
|
412
|
+
if (res?.ok === false) throw new Error(formatBashFailure(command, res));
|
|
427
413
|
|
|
428
|
-
return
|
|
414
|
+
return markTruncatedOutput(res, unwrapValue(res));
|
|
429
415
|
};
|
|
430
416
|
|
|
431
417
|
return { read, write, edit, bash, nova };
|
|
@@ -446,22 +432,7 @@ function makeConsole(runId, limits) {
|
|
|
446
432
|
return; }
|
|
447
433
|
|
|
448
434
|
count++;
|
|
449
|
-
|
|
450
|
-
const line = args
|
|
451
|
-
.map((a) => {
|
|
452
|
-
if (isString(a)) return a;
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
const plain = toPlain(a);
|
|
456
|
-
const encoded = JSON.stringify(plain);
|
|
457
|
-
|
|
458
|
-
return encoded === undefined ? String(plain) : encoded;
|
|
459
|
-
} catch {
|
|
460
|
-
return String(a);
|
|
461
|
-
}
|
|
462
|
-
})
|
|
463
|
-
.join(" ");
|
|
464
|
-
|
|
435
|
+
const line = args.map(encodeConsoleArg).join(" ");
|
|
465
436
|
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
466
437
|
|
|
467
438
|
if (clipped.truncated) markTruncated();
|
|
@@ -477,16 +448,55 @@ function postFailure(runId, err, location) {
|
|
|
477
448
|
post({ op: "error", runId, message, location });
|
|
478
449
|
}
|
|
479
450
|
|
|
451
|
+
function denyBuiltin(id) {
|
|
452
|
+
if (isDeniedGuestImport(id)) throw new Error(guestImportMessage(id));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let sealedRealm = false;
|
|
456
|
+
|
|
457
|
+
function sealGuestRealm() {
|
|
458
|
+
if (sealedRealm) return;
|
|
459
|
+
sealedRealm = true;
|
|
460
|
+
if (isFunction(process.getBuiltinModule)) {
|
|
461
|
+
const orig = process.getBuiltinModule.bind(process);
|
|
462
|
+
process.getBuiltinModule = (id) => {
|
|
463
|
+
denyBuiltin(id);
|
|
464
|
+
|
|
465
|
+
return orig(id);
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isFunction(process.binding)) {
|
|
470
|
+
process.binding = (id) => {
|
|
471
|
+
throw new Error(guestImportMessage(String(id)));
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (isFunction(process.dlopen)) {
|
|
476
|
+
process.dlopen = () => {
|
|
477
|
+
throw new Error("guest cannot load native modules; use read, edit, write, or bash");
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (isFunction(process.kill)) {
|
|
482
|
+
// Signals are process-wide: process.kill escapes the worker thread and can
|
|
483
|
+
// terminate the host, so it stays out of the guest. Stop things with bash.
|
|
484
|
+
process.kill = () => {
|
|
485
|
+
throw new Error("process.kill is not available in guest programs; stop processes with bash");
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
480
490
|
async function handleRun(msg) {
|
|
481
491
|
const { runId, prepared, limits, available, batchRead = true } = msg;
|
|
482
492
|
activeRunId = runId;
|
|
483
493
|
runActive = true;
|
|
484
494
|
let compiled;
|
|
495
|
+
sealGuestRealm();
|
|
485
496
|
|
|
486
497
|
try {
|
|
487
498
|
// Existing programs may declare their own data variable; bind it only when supplied.
|
|
488
|
-
|
|
489
|
-
compiled = { fn: new AsyncFunction(...bindings, prepared.body), hasReturn: prepared.hasReturn };
|
|
499
|
+
compiled = compileGuest(prepared, msg.data);
|
|
490
500
|
} catch (err) {
|
|
491
501
|
postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran. When passing data, do not redeclare its binding."));
|
|
492
502
|
|
|
@@ -502,14 +512,7 @@ async function handleRun(msg) {
|
|
|
502
512
|
);
|
|
503
513
|
|
|
504
514
|
if (runId !== activeRunId) return;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
try {
|
|
508
|
-
plain = toPlain(value);
|
|
509
|
-
} catch (err) {
|
|
510
|
-
plain = "[unserializable: " + (err?.message || err) + "]";
|
|
511
|
-
}
|
|
512
|
-
|
|
515
|
+
const plain = plainGuestValue(value);
|
|
513
516
|
runActive = false;
|
|
514
517
|
post({ op: "done", runId, value: plain, undefinedReturn: value === undefined && !compiled.hasReturn, hasReturn: compiled.hasReturn });
|
|
515
518
|
} catch (err) {
|
|
@@ -522,13 +525,7 @@ parentPort.on("message", (msg) => {
|
|
|
522
525
|
if (!isObject(msg)) return;
|
|
523
526
|
|
|
524
527
|
if (msg.op === "rpc:result") {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (!pending) return;
|
|
528
|
-
pendingRpc.delete(msg.id);
|
|
529
|
-
|
|
530
|
-
if (msg.ok) pending.resolve(msg.value);
|
|
531
|
-
else pending.reject(new Error(msg.error));
|
|
528
|
+
handleRpcResult(msg);
|
|
532
529
|
|
|
533
530
|
return;
|
|
534
531
|
}
|