pi-supernova 0.10.1 → 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 +376 -221
- package/docs/CHANGELOG.md +107 -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 +39 -19
- 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 +25 -9
- 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 +86 -17
- 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/runtime/parallel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isFunction, isObject, isString } from "../shared/decode.js";
|
|
2
2
|
|
|
3
|
-
const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "evidence", "surface", "asgrep_search", "asgrep_status", "ast_grep", "web_search"]);
|
|
3
|
+
const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "evidence", "surface", "asgrep_search", "asgrep_status", "ast_grep", "web_search", "isearch"]);
|
|
4
4
|
|
|
5
5
|
const READ_ONLY_LSP = new Set(["definition", "references", "hover", "symbols", "diagnostics", "implementation", "type_definition", "incoming_calls", "outgoing_calls"]);
|
|
6
6
|
|
|
@@ -55,9 +55,11 @@ function compatibleScheduledJob(job, active) {
|
|
|
55
55
|
|
|
56
56
|
async function prepareScheduledJob(job) {
|
|
57
57
|
if (!job.resolveKey) return;
|
|
58
|
+
|
|
58
59
|
// Key lookup is advisory. Invalid paths still run in isolation so the real
|
|
59
60
|
// adapter reports the error through its normal trace/permission checks.
|
|
60
61
|
try { job.key = await job.resolveKey(); } catch {}
|
|
62
|
+
|
|
61
63
|
job.resolveKey = undefined;
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -98,6 +100,7 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
98
100
|
if (!active.size) stats.readWaves++;
|
|
99
101
|
stats.peakParallelReads = Math.max(stats.peakParallelReads, active.size + 1);
|
|
100
102
|
}
|
|
103
|
+
|
|
101
104
|
active.add(job);
|
|
102
105
|
// Each promise settles independently. Barriers still wait for every active
|
|
103
106
|
// sibling, including when Promise.all in the guest has already rejected.
|
|
@@ -107,15 +110,20 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
107
110
|
async function drain() {
|
|
108
111
|
if (draining) return;
|
|
109
112
|
draining = true;
|
|
113
|
+
|
|
110
114
|
try {
|
|
111
115
|
while (queue.length && active.size < maxParallelReads) {
|
|
112
116
|
const job = queue[0];
|
|
117
|
+
|
|
113
118
|
if (job.cancelled) { queue.shift(); continue; }
|
|
119
|
+
|
|
114
120
|
// Resolve identities after shell/override barriers, which may change
|
|
115
121
|
// symlinks. Check synchronously: a finishing job must not lose its wakeup.
|
|
116
122
|
if (job.resolveKey && [...active].some(running => !isString(running.key))) break;
|
|
117
123
|
await prepareScheduledJob(job);
|
|
124
|
+
|
|
118
125
|
if (job.cancelled) continue;
|
|
126
|
+
|
|
119
127
|
if (!compatibleScheduledJob(job, active)) break;
|
|
120
128
|
queue.shift();
|
|
121
129
|
start(job);
|
|
@@ -143,51 +151,3 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
143
151
|
},
|
|
144
152
|
};
|
|
145
153
|
}
|
|
146
|
-
|
|
147
|
-
function requireArray(value, name) {
|
|
148
|
-
if (!Array.isArray(value)) throw new TypeError(name + " requires an array");
|
|
149
|
-
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function waveMutates(list, meta, config) {
|
|
154
|
-
const names = meta?.names ?? [];
|
|
155
|
-
|
|
156
|
-
return names.length !== list.length || names.some((name, i) => isMutatingTool(name, config, meta?.calls?.[i]?.args, meta?.definitions?.[i]));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function shouldRunParallel(mutating, mode, length) {
|
|
160
|
-
return !mutating && (mode === "parallel" || (mode === "auto" && length > 1));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function settleParallel(list) {
|
|
164
|
-
// Do not finish a wave while its already-started host calls are still running.
|
|
165
|
-
const settled = await Promise.allSettled(list.map(thunk => Promise.resolve().then(thunk)));
|
|
166
|
-
const failure = settled.find(item => item.status === "rejected");
|
|
167
|
-
|
|
168
|
-
if (failure) throw failure.reason;
|
|
169
|
-
|
|
170
|
-
return { results: settled.map(item => item.value), mode: "parallel", reason: "independent-reads" };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function runSerial(list, mutating) {
|
|
174
|
-
const results = [];
|
|
175
|
-
|
|
176
|
-
for (const thunk of list) results.push(await thunk());
|
|
177
|
-
|
|
178
|
-
return { results, mode: "serial", reason: mutating ? "mutating" : "single-or-forced" };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function runParallelWave(thunks, meta, options = {}) {
|
|
182
|
-
const list = requireArray(thunks, "parallel wave");
|
|
183
|
-
|
|
184
|
-
if (list.some(item => !isFunction(item))) throw new TypeError("parallel wave requires functions");
|
|
185
|
-
|
|
186
|
-
if (!list.length) return { results: [], mode: "serial", reason: "empty" };
|
|
187
|
-
const { mode = "auto", config = {} } = options;
|
|
188
|
-
const mutating = waveMutates(list, meta, config);
|
|
189
|
-
|
|
190
|
-
if (shouldRunParallel(mutating, mode, list.length)) return settleParallel(list);
|
|
191
|
-
|
|
192
|
-
return runSerial(list, mutating);
|
|
193
|
-
}
|
|
@@ -13,8 +13,9 @@ const mutationTotals = results => results.reduce((total, result) => {
|
|
|
13
13
|
return total;
|
|
14
14
|
}, {committed:0,rolledBack:0,external:0,pendingCommits:0,recoveryFailed:false});
|
|
15
15
|
|
|
16
|
-
export function programBatchText(results, total, stopped = "", failed = 0) {
|
|
16
|
+
export function programBatchText(results, total, stopped = "", failed = 0, texts = results.map(textOf)) {
|
|
17
17
|
const m = mutationTotals(results);
|
|
18
|
+
|
|
18
19
|
const summary = stopped
|
|
19
20
|
? "error: programs stopped: " + stopped + " " + results.length + "/" + total
|
|
20
21
|
: failed > 0
|
|
@@ -22,10 +23,8 @@ export function programBatchText(results, total, stopped = "", failed = 0) {
|
|
|
22
23
|
: "ok: programs " + results.length + "/" + total;
|
|
23
24
|
|
|
24
25
|
return summary +
|
|
25
|
-
(m.recoveryFailed || m.pendingCommits ? "; filesystem outcome uncertain: inspect disk" : "") + "\nresults (UTF-16 lengths):\n" +
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return "[" + i + "] " + text.length + "\n" + text + "\n";
|
|
26
|
+
(m.recoveryFailed || m.pendingCommits ? "; filesystem outcome uncertain: inspect disk" : "") + "\nresults (UTF-16 lengths):\n" + texts.map((text,i) => {
|
|
27
|
+
return "[" + (results[i].details?.programIndex ?? i) + "] " + text.length + "\n" + text + "\n";
|
|
29
28
|
}).join("");
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -92,18 +91,16 @@ class ProgramBatch {
|
|
|
92
91
|
return " (ran " + Math.round(performance.now() - this.started) + "ms of " + this.timeout + "ms)";
|
|
93
92
|
}
|
|
94
93
|
takeSettled(result, i) {
|
|
95
|
-
this.results.push(result);
|
|
94
|
+
this.results.push({...result,details:{...result.details,programIndex:i}});
|
|
96
95
|
this.trace.push(...(result.details?.trace ?? []));
|
|
97
96
|
this.collectImages(result, i);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
parallelBudgetStop(settled) {
|
|
101
|
-
const results = settled.filter(Boolean);
|
|
102
100
|
const {images, bytes} = imageTotals(settled);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return kind ? "batch " + kind + " budget exceeded; completed commits remain" : "";
|
|
101
|
+
|
|
102
|
+
return images > 16 || bytes > 20 * 1024 * 1024
|
|
103
|
+
? "batch image budget exceeded; completed commits remain" : "";
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
async runParallel() {
|
|
@@ -116,7 +113,9 @@ class ProgramBatch {
|
|
|
116
113
|
const i = next++;
|
|
117
114
|
settled[i] = await this.runOne(this.programs[i], i);
|
|
118
115
|
this.live[i] = [];
|
|
119
|
-
this.stopped ||= this.parallelBudgetStop(settled);
|
|
116
|
+
this.stopped ||= this.parallelBudgetStop(settled) || this.sequentialStop(settled[i], i);
|
|
117
|
+
|
|
118
|
+
if (settled[i].details?.mutations?.recoveryFailed || settled[i].details?.mutations?.pendingCommits) this.controller.abort(new Error("batch stopped: filesystem outcome uncertain"));
|
|
120
119
|
}
|
|
121
120
|
}));
|
|
122
121
|
|
|
@@ -125,7 +124,7 @@ class ProgramBatch {
|
|
|
125
124
|
this.takeSettled(settled[i], i);
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
if ((!this.stopped && settled.includes(undefined)) || this.combined.aborted || performance.now() >= this.deadline) this.stopped
|
|
127
|
+
if ((!this.stopped && settled.includes(undefined)) || this.combined.aborted || performance.now() >= this.deadline) this.stopped ||= "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
sequentialStop(result, i) {
|
|
@@ -133,11 +132,9 @@ class ProgramBatch {
|
|
|
133
132
|
|
|
134
133
|
if (this.imageDropped) stopped = "batch image budget exceeded; remaining programs did not run";
|
|
135
134
|
|
|
136
|
-
if (result.details?.
|
|
137
|
-
//
|
|
138
|
-
// unless a real execution/resource limit stops it;
|
|
139
|
-
|
|
140
|
-
if (result.details?.logTruncated) stopped ||= "batch log budget exceeded; remaining programs did not run; earlier commits remain";
|
|
135
|
+
if (!result.details?.mutations || result.details.mutations.recoveryFailed || result.details.mutations.pendingCommits || /host call budget exceeded/.test(result.details?.error ?? "")) stopped = "program " + (i+1) + " exhausted a shared budget or has an uncertain filesystem outcome; remaining programs did not run; earlier commits remain";
|
|
136
|
+
// Text/log clipping is not an execution failure. Finish every requested
|
|
137
|
+
// entry unless a real execution/resource limit stops it; delivery is bounded.
|
|
141
138
|
|
|
142
139
|
// The deadline explains a killed program better than "program N failed".
|
|
143
140
|
if (this.combined.aborted || performance.now() >= this.deadline) stopped = "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
@@ -159,30 +156,56 @@ class ProgramBatch {
|
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
failNote(failed) {
|
|
162
|
-
return this.stopped || (
|
|
159
|
+
return this.stopped || (failed ? failed + " program" + (failed>1?"s":"") + " failed" : undefined);
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
boundedText(failed) {
|
|
166
|
-
const note =
|
|
162
|
+
boundedText(failed, logTruncated) {
|
|
163
|
+
const note = [
|
|
164
|
+
this.imageDropped && !this.stopped ? "some images dropped: batch image budget" : "",
|
|
165
|
+
logTruncated ? "logs truncated: batch log budget" : "",
|
|
166
|
+
].filter(Boolean).join("; ");
|
|
167
|
+
|
|
168
|
+
const limit = Math.max(0, this.config.maxReturnChars - this.imageTextChars());
|
|
169
|
+
const texts = this.results.map(textOf);
|
|
170
|
+
const render = previews => programBatchText(this.results, this.programs.length, this.stopped, failed, previews) + (note ? "; " + note : "");
|
|
171
|
+
const text = render(texts);
|
|
172
|
+
|
|
173
|
+
if (text.length <= limit) return { text, truncated: false };
|
|
174
|
+
const marker = "\n[batch output truncated; per-program previews]";
|
|
175
|
+
// Reserve framing and length digits before dividing space. Clipping the
|
|
176
|
+
// combined string can erase an entire program, including its error reason.
|
|
177
|
+
const overhead = render(texts.map(() => "")).length + marker.length + texts.length * String(limit).length;
|
|
178
|
+
const share = Math.max(0, Math.floor((limit - overhead) / Math.max(1, texts.length)));
|
|
179
|
+
const previews = texts.map(value => truncateChars(value, share, "program output").text);
|
|
167
180
|
|
|
168
|
-
return truncateChars(
|
|
181
|
+
return { ...truncateChars(render(previews) + marker, limit, "batch output"), truncated: true };
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
finish() {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const
|
|
185
|
+
const failedPrograms = [];
|
|
186
|
+
|
|
187
|
+
for (const result of this.results) {
|
|
188
|
+
if (result.details?.ok === false) failedPrograms.push(result.details.programIndex);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const attempted = new Set(this.results.map(result => result.details.programIndex));
|
|
192
|
+
const notRunPrograms = this.programs.flatMap((_, i) => attempted.has(i) ? [] : [i]);
|
|
193
|
+
const failed = failedPrograms.length;
|
|
175
194
|
const logs = this.results.flatMap(result => result.details?.logs ?? []);
|
|
176
195
|
const logLimit = this.config.maxLogLines ?? 100;
|
|
196
|
+
const logTruncated = logs.length > logLimit || this.results.some(result=>result.details?.logTruncated);
|
|
197
|
+
const bounded = this.boundedText(failed, logTruncated);
|
|
198
|
+
const mutations = mutationTotals(this.results);
|
|
199
|
+
const content = [{type:"text",text:bounded.text}];
|
|
177
200
|
this.images.forEach((image,i) => content.push({type:"text",text:this.imageLabels[i]},image));
|
|
178
201
|
|
|
179
202
|
// Return a typed stop report instead of throwing away earlier results/images.
|
|
180
203
|
// Single-program errors retain their existing throwing behavior.
|
|
181
|
-
return {content,isError:!!this.stopped ||
|
|
182
|
-
programs:this.results,attempted:this.results.length,total:this.programs.length,stopped:this.stopped,parallel:this.parallel,
|
|
204
|
+
return {content,isError:!!this.stopped || failed>0,details:{ok:!this.stopped && !failed,partial:failed>0 && mutations.committed>0,error:this.failNote(failed),wallMs:Math.round(performance.now()-this.started),
|
|
205
|
+
programs:this.results,attempted:this.results.length,failed,failedPrograms,notRunPrograms,notRun:this.programs.length-this.results.length,total:this.programs.length,stopped:this.stopped,parallel:this.parallel,
|
|
183
206
|
result:bounded.truncated ? bounded.text : this.results.map(result=>result.details?.result),
|
|
184
207
|
returnTruncated:bounded.truncated || this.results.some(result=>result.details?.returnTruncated),
|
|
185
|
-
logTruncated
|
|
208
|
+
logTruncated,logs:logs.slice(0,logLimit),trace:this.trace,mutations}};
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
async run() {
|
|
@@ -208,11 +231,13 @@ function imageBlocks(result) {
|
|
|
208
231
|
|
|
209
232
|
function imageTotals(settled) {
|
|
210
233
|
let images = 0, bytes = 0;
|
|
234
|
+
|
|
211
235
|
for (const result of settled) {
|
|
212
236
|
for (const block of imageBlocks(result)) {
|
|
213
237
|
images++;
|
|
214
238
|
bytes += Buffer.byteLength(block.data, "base64");
|
|
215
239
|
}
|
|
216
240
|
}
|
|
241
|
+
|
|
217
242
|
return {images, bytes};
|
|
218
243
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readLimitedBytes } from "../fs/file-io.js";
|
|
1
|
+
import { readLimitedBytes, sameFileVersion } from "../fs/file-io.js";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { resolveWorkspacePath } from "../fs/workspace.js";
|
|
4
4
|
|
|
@@ -24,6 +24,7 @@ export async function readProgramFile(file, cwd, maxChars, signal) {
|
|
|
24
24
|
|
|
25
25
|
const bytes = await readLimitedBytes(handle, stat, maxBytes, file, signal, tooLarge);
|
|
26
26
|
|
|
27
|
+
if (!sameFileVersion(stat, await handle.stat())) throw new Error("program file changed while reading: " + file + "; retry with a stable source file");
|
|
27
28
|
signal?.throwIfAborted();
|
|
28
29
|
// Do not silently replace invalid bytes in executable source. Preserve BOMs.
|
|
29
30
|
let code;
|
package/src/runtime/program.js
CHANGED
|
@@ -6,16 +6,6 @@ const PARSE_OPTIONS = { ecmaVersion: "latest", sourceType: "module", allowReturn
|
|
|
6
6
|
|
|
7
7
|
const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
8
8
|
|
|
9
|
-
function hasReturn(node) {
|
|
10
|
-
if (!isObject(node)) return false;
|
|
11
|
-
|
|
12
|
-
if (node.type === "ReturnStatement") return true;
|
|
13
|
-
|
|
14
|
-
if (FUNCTION_TYPES.has(node.type)) return false;
|
|
15
|
-
|
|
16
|
-
return Object.values(node).some(value => Array.isArray(value) ? value.some(hasReturn) : hasReturn(value));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
function parseExpressionFunction(code) {
|
|
20
10
|
try {
|
|
21
11
|
const program = parse(code, PARSE_OPTIONS);
|
|
@@ -24,7 +14,7 @@ function parseExpressionFunction(code) {
|
|
|
24
14
|
const candidate = statement?.type === "ExpressionStatement" ? statement.expression : statement;
|
|
25
15
|
|
|
26
16
|
if (candidate && FUNCTION_TYPES.has(candidate.type)) {
|
|
27
|
-
return { program, expression: candidate, expressionSource: code.slice(statement.start, statement.end).replace(/;\s*$/, "") };
|
|
17
|
+
return { program, expression: candidate, sourceStart: statement.start, expressionSource: code.slice(statement.start, statement.end).replace(/;\s*$/, "") };
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
return { program };
|
|
@@ -44,28 +34,57 @@ function parseExpressionFunction(code) {
|
|
|
44
34
|
|
|
45
35
|
function deniedSpecifier(node) {
|
|
46
36
|
if (node?.type === "Literal" && isString(node.value)) return node.value;
|
|
37
|
+
|
|
47
38
|
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value?.cooked;
|
|
48
39
|
}
|
|
49
40
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
function inspectProgram(root, expression) {
|
|
42
|
+
let returns = expression?.type === "ArrowFunctionExpression" && expression.body.type !== "BlockStatement";
|
|
43
|
+
// Valid member chains can exceed the host call stack well below the code cap.
|
|
44
|
+
const pending = [{node:root,returnScope:true}];
|
|
45
|
+
|
|
46
|
+
while (pending.length) {
|
|
47
|
+
let {node,returnScope} = pending.pop();
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(node)) {
|
|
50
|
+
for (let i = node.length - 1; i >= 0; i--) pending.push({node:node[i],returnScope});
|
|
51
|
+
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line anti-slop/no-runtime-typeof -- This is an Acorn-owned AST, not unparsed host or guest data.
|
|
56
|
+
if (!node || typeof node !== "object") continue;
|
|
57
|
+
const type = node.type;
|
|
58
|
+
|
|
59
|
+
// Function scopes affect only return hints. Import checks must still visit
|
|
60
|
+
// nested/unreachable bodies, even after finding a return in the invoked scope.
|
|
61
|
+
if (FUNCTION_TYPES.has(type)) returnScope = node === expression;
|
|
62
|
+
|
|
63
|
+
if (type === "ReturnStatement" && returnScope) returns = true;
|
|
64
|
+
|
|
65
|
+
if (type === "ImportDeclaration" || type === "ImportExpression") rejectGuestImport(node.source);
|
|
66
|
+
|
|
67
|
+
if (type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "require") rejectGuestImport(node.arguments?.[0]);
|
|
68
|
+
|
|
69
|
+
const keys = Object.keys(node);
|
|
70
|
+
|
|
71
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
72
|
+
const value = node[keys[i]];
|
|
73
|
+
|
|
74
|
+
// eslint-disable-next-line anti-slop/no-runtime-typeof -- Acorn already parsed these fields; distinguish child objects from scalar AST metadata.
|
|
75
|
+
if (value && typeof value === "object") pending.push({node:value,returnScope});
|
|
76
|
+
}
|
|
57
77
|
}
|
|
78
|
+
|
|
79
|
+
return returns;
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
function prepareProgram(code) {
|
|
61
83
|
const parsed = parseExpressionFunction(code);
|
|
62
|
-
|
|
84
|
+
const returns = inspectProgram(parsed.program ?? parsed.expression, parsed.expression);
|
|
63
85
|
const body = parsed.expression ? "return await (" + parsed.expressionSource + "\n)();" : code;
|
|
64
|
-
const returns = parsed.expression
|
|
65
|
-
? parsed.expression.type === "ArrowFunctionExpression" && parsed.expression.body.type !== "BlockStatement" || hasReturn(parsed.expression.body)
|
|
66
|
-
: hasReturn(parsed.program);
|
|
67
86
|
|
|
68
|
-
return { body, hasReturn: returns };
|
|
87
|
+
return { body, hasReturn: returns, sourceStart: parsed.sourceStart ?? 0, prefixLength: parsed.expression ? "return await (".length : 0 };
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
function admitData(data, cap) {
|
|
@@ -75,6 +94,7 @@ function admitData(data, cap) {
|
|
|
75
94
|
const encoded = JSON.stringify(data);
|
|
76
95
|
|
|
77
96
|
if (encoded === undefined) return { error: "data must be JSON-serializable" };
|
|
97
|
+
|
|
78
98
|
if (encoded.length > cap) return { error: "data exceeds " + cap + " characters (serialized JSON: " + encoded.length + " UTF-16 characters); no commands ran. Split literal inputs across invocations; large text can use write({path,content,append:true}) chunks without omitting content" };
|
|
79
99
|
|
|
80
100
|
return { data: JSON.parse(encoded) };
|
|
@@ -83,7 +103,9 @@ function admitData(data, cap) {
|
|
|
83
103
|
|
|
84
104
|
function admitCode({ code, file, cap }) {
|
|
85
105
|
if ((code === undefined) === (file === undefined)) return { error: "supply exactly one of code or file; no commands ran" };
|
|
106
|
+
|
|
86
107
|
if (file === undefined && (!isString(code) || !code.trim())) return { error: "code must be a non-empty string" };
|
|
108
|
+
|
|
87
109
|
if (file === undefined && code.length > cap) return { error: "code exceeds " + cap + " characters; split large writes into write({path,content,append:true}) chunks" };
|
|
88
110
|
}
|
|
89
111
|
|
|
@@ -109,6 +131,7 @@ function admitGuest({ code, file, data, config }) {
|
|
|
109
131
|
|
|
110
132
|
return { data: admitted.data, timeoutMs: timeout.timeoutMs };
|
|
111
133
|
}
|
|
134
|
+
|
|
112
135
|
export { admitGuest, prepareProgram };
|
|
113
136
|
|
|
114
137
|
function rejectGuestImport(source) {
|
|
@@ -117,25 +140,55 @@ function rejectGuestImport(source) {
|
|
|
117
140
|
throw new Error(reason + "; no commands ran");
|
|
118
141
|
}
|
|
119
142
|
|
|
120
|
-
function containingAwait(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
function containingAwait(root, offset) {
|
|
144
|
+
const pending = [root];
|
|
145
|
+
let found = null;
|
|
146
|
+
|
|
147
|
+
while (pending.length) {
|
|
148
|
+
const node = pending.pop();
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(node)) {
|
|
151
|
+
for (const child of node) pending.push(child);
|
|
152
|
+
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!isObject(node) || offset < node.start || offset >= node.end) continue;
|
|
157
|
+
|
|
158
|
+
// Containing awaits nest; descendants supersede the enclosing await.
|
|
159
|
+
if (node.type === "AwaitExpression") found = node;
|
|
160
|
+
|
|
161
|
+
for (const child of Object.values(node)) pending.push(child);
|
|
126
162
|
}
|
|
127
|
-
|
|
163
|
+
|
|
164
|
+
return found;
|
|
128
165
|
}
|
|
129
166
|
|
|
130
|
-
// V8 points at 'await'; JSC points at the
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
// V8 points at 'await'; JSC points at the call parenthesis. Resolve the await
|
|
168
|
+
// in executable syntax, then undo wrapper/sliced-comment offsets. ECMAScript
|
|
169
|
+
// line separators differ from file/JSONL LF framing.
|
|
170
|
+
export function normalizeGuestLocation(source, location, prepared = { body: source }) {
|
|
171
|
+
if (!location) return location;
|
|
172
|
+
const body = prepared.body;
|
|
173
|
+
const lines = /\r\n|[\n\r\u2028\u2029]/g;
|
|
174
|
+
let start = 0;
|
|
175
|
+
|
|
176
|
+
for (let line = 1; line < location.line; line++) {
|
|
177
|
+
if (!lines.exec(body)) return null;
|
|
178
|
+
start = lines.lastIndex;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let offset = start + location.col - 1;
|
|
182
|
+
|
|
183
|
+
if (location.awaited) {
|
|
184
|
+
try { offset = containingAwait(parse(body, PARSE_OPTIONS), offset)?.start ?? offset; }
|
|
185
|
+
catch { /* Keep the engine's position when syntax cannot be recovered. */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
offset += (prepared.sourceStart ?? 0) - (prepared.prefixLength ?? 0);
|
|
189
|
+
|
|
190
|
+
if (offset < 0 || offset > source.length) return null;
|
|
191
|
+
const original = source.slice(0, offset).split(/\r\n|[\n\r\u2028\u2029]/);
|
|
192
|
+
|
|
193
|
+
return { ...location, line: original.length, col: original.at(-1).length + 1 };
|
|
141
194
|
}
|
package/src/runtime/reference.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Standing tool description: sent on every request. No result or history compression.
|
|
2
|
-
export const REFERENCE = `JS body/async arrow; read/write/edit/bash, no fs/import/require. file: workspace JS; data:
|
|
2
|
+
export const REFERENCE = `JS body/async arrow; read/write/edit/bash, no fs/import/require. file: workspace JS; data: ≤48000 JSON chars.
|
|
3
3
|
read(path|paths,offset=1,limit?) → text/text[]; dirs→entries; PNG/JPEG/GIF/WebP ≤16/20MiB.
|
|
4
|
-
Text ≤64 MiB internally; complete:true
|
|
5
|
-
read({path,json:true})
|
|
4
|
+
Text ≤64 MiB internally; complete:true whole file. Display capped: summarize or read(path,{offset:1,limit:80}); larger files via bash.
|
|
5
|
+
read({path,json:true}) → JSON; json:".field" or json:[".a"] selects (16MiB input/64MiB selection; no jq). Values retain their types; leftover selector folds into json.
|
|
6
|
+
read({query,indexed:true}): optional callable isearch, then an owned editable read.
|
|
6
7
|
read("symbol or question") = read({query,resolve:true}) → view; check status; view.text is a span. read(path,{about}) windows; read({query,evidence:true}) evidence; read({path,outline:true}) declarations.
|
|
7
|
-
write/edit workspace-only; external changes need separately authorized command. write(path,text) or {path,content,append:true}; after read edit or replace:true.
|
|
8
|
-
edit(path,oldText,newText) or {path,edits:[{oldText,newText}]}; unique exact match; numbered windows. edit(view,text) or edit(view,old,new). edit(async()=>{...}) checkpoint: no bash;
|
|
9
|
-
bash(command,
|
|
10
|
-
bash({command,background:true,pty?:true,timeoutMs?:1800000})→sessionId. bash({action:"list"}) or {sessionId,action:"poll"|"write"|"stop",cursor?,waitMs?,input?}→status/output/cursor/exitCode. PTY macOS/Linux; jobs end at shutdown.
|
|
11
|
-
programs:[{code?,file?,data?}] inherit defaults; mergeData:true shallow. Fresh guests
|
|
12
|
-
Promise.all ≤8 disjoint
|
|
8
|
+
write/edit workspace-only; external changes need separately authorized command. write(path,text,opts?) or {path,content,append:true}; after read edit or replace:true.
|
|
9
|
+
edit(path,oldText,newText) or {path,edits:[{oldText,newText}]}; unique exact match; numbered windows. edit(view,text) or edit(view,old,new). edit(async()=>{...}) atomic checkpoint: no bash; commit/rollback+rethrow. Other edits save; retry failures only.
|
|
10
|
+
bash(command,opts?) or {command,args,cwd?,timeoutMs?}; nonzero throws. Capture≤2Mi chars; larger output via file; outer deadline.
|
|
11
|
+
bash({command,background:true,pty?:true,timeoutMs?:1800000})→sessionId. bash({action:"list"}) or {sessionId,action:"poll"|"write"|"stop",cursor?,waitMs?,input?}→status/output/cursor/exitCode. poll waitMs 0..30000. PTY macOS/Linux; jobs end at shutdown.
|
|
12
|
+
programs:[{code?,file?,data?}] inherit defaults; mergeData:true shallow. Fresh guests; failures continue; limits/uncertainty stop; parallel:true disjoint.
|
|
13
|
+
Promise.all ≤8 disjoint; same-file serial; bash/checkpoints barriers. Independent: Promise.allSettled; uncaught errors stop JS.
|
|
13
14
|
`;
|