pi-supernova 0.8.1 → 0.8.2
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 +64 -11
- package/docs/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/adapters/bash.js +10 -4
- package/src/contract/bash.js +12 -1
- package/src/fs/text-ops.js +0 -15
- package/src/fs/workspace.js +5 -1
- package/src/runtime/guest-worker.js +1 -1
- package/src/runtime/program-batch.js +24 -3
- package/src/runtime/reference.js +9 -9
- package/src/runtime/runtime.js +17 -8
package/README.md
CHANGED
|
@@ -12,7 +12,35 @@ Ordinary JavaScript control flow remains available; the guest command bindings
|
|
|
12
12
|
are only `read`, `edit`, `write`, and `bash`. Supernova supplies retrieval,
|
|
13
13
|
transactional file operations, batching, bounded results and the grouped nova UI.
|
|
14
14
|
|
|
15
|
-
## What is new in 0.8.
|
|
15
|
+
## What is new in 0.8.2
|
|
16
|
+
|
|
17
|
+
This patch release fixes shell failure handling, cancellation and parallel-batch
|
|
18
|
+
limits, and makes one-call batching guidance explicit.
|
|
19
|
+
|
|
20
|
+
- **Shell quoting stays intact:** quoted executable paths are no longer unwrapped.
|
|
21
|
+
Shell syntax errors suggest literal `bash({command,args})` with `data` for
|
|
22
|
+
embedded scripts, or a quoted heredoc. Commands are not rewritten or retried.
|
|
23
|
+
- **Validation before commit:** invalid timeouts and null-byte
|
|
24
|
+
arguments are rejected before the shell boundary flushes staged files.
|
|
25
|
+
- **Useful failure output:** long command labels are bounded so the original
|
|
26
|
+
stderr is not crowded out by a repeated script.
|
|
27
|
+
- **Timeouts retain diagnostics:** the outer program deadline stops the worker
|
|
28
|
+
and gives pending host calls a bounded drain to retain shell output. Explicit
|
|
29
|
+
cancellation is reported separately from timeout. The outer `timeoutMs` covers
|
|
30
|
+
every wait and command, including `sleep`.
|
|
31
|
+
- **Parallel budgets fail honestly:** exceeding the shared output, log or image
|
|
32
|
+
allowance marks the batch failed and stops queued entries. Already-running
|
|
33
|
+
entries settle; their results and completed commits remain. Aggregate logs stay
|
|
34
|
+
capped rather than multiplying the allowance per guest.
|
|
35
|
+
- **Batch known work in one call:** combine independent reads/checks with
|
|
36
|
+
`Promise.all`, then sequence edits and verification in the same program. Use
|
|
37
|
+
another invocation when returned evidence is needed for the next decision.
|
|
38
|
+
|
|
39
|
+
Verified on **macOS / Node 26.7**: 267 package tests (384 repository tests),
|
|
40
|
+
2,328 stress invocations, actual Pi/OMP host checks, lint and both token-budget
|
|
41
|
+
checks. This is not a claim of exhaustive platform or formal mutation testing.
|
|
42
|
+
|
|
43
|
+
## 0.8.0 features and measurements
|
|
16
44
|
|
|
17
45
|
- **Shared program source:** top-level `code` or `file` supplies a batch default;
|
|
18
46
|
entries may override it. A shared program is sent once instead of in every entry,
|
|
@@ -81,14 +109,12 @@ Local checkout installs are for development, not distribution:
|
|
|
81
109
|
pi install /path/to/pi-stack/packages/pi-supernova
|
|
82
110
|
```
|
|
83
111
|
|
|
84
|
-
Git pushes do not update npm installations. Publish the new npm version first
|
|
85
|
-
then reinstall it in
|
|
86
|
-
range excludes the new minor version (`^0.6.0` excludes `0.7.0`). After 0.7.0 is
|
|
87
|
-
published, pin that release with:
|
|
112
|
+
Git pushes do not update npm installations. Publish the new npm version first,
|
|
113
|
+
then reinstall it in your host. To pin **0.8.2** once it is published:
|
|
88
114
|
|
|
89
115
|
```bash
|
|
90
|
-
pi install npm:pi-supernova@0.
|
|
91
|
-
omp install npm:pi-supernova@0.
|
|
116
|
+
pi install npm:pi-supernova@0.8.2
|
|
117
|
+
omp install npm:pi-supernova@0.8.2
|
|
92
118
|
```
|
|
93
119
|
|
|
94
120
|
In Pi, `pi list` shows the configured package sources. A local path uses that
|
|
@@ -135,6 +161,21 @@ payloads are not repeated in owned direct-execution errors;
|
|
|
135
161
|
stdout/stderr, exit status and source context remain. Session environment variables are taken
|
|
136
162
|
from the current execution context, not inherited from a different parent session.
|
|
137
163
|
|
|
164
|
+
Shell strings are executed unchanged, including quoted executable paths. For inline
|
|
165
|
+
Python/Node scripts, prefer literal argv with `data` instead of nested shell quotes:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"code": "return await bash({command:\"python3\",args:[\"-c\",data.script]});",
|
|
170
|
+
"data": {"script": "q = {'name': 'example'}\nprint(f\"{q['name']}\")\n"}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Shell syntax errors keep the original diagnostic and suggest argv or a quoted
|
|
175
|
+
heredoc; commands are never automatically rewritten or retried. Invalid timeouts
|
|
176
|
+
and null-byte arguments fail before flushing staged changes. Failure labels are
|
|
177
|
+
bounded so a large script cannot crowd out its stderr.
|
|
178
|
+
|
|
138
179
|
Source questions locate a declaration in one command. An exact
|
|
139
180
|
declaration match uses one bounded direct ripgrep search, without a prerequisite
|
|
140
181
|
file listing, persistent index, embeddings or summarization. A transient filename
|
|
@@ -349,6 +390,9 @@ Set `parallel: true` with `programs` to run independent entries concurrently
|
|
|
349
390
|
in submission order. A failed entry does not stop siblings. Two entries writing
|
|
350
391
|
the same file race: the losing commit reports a conflict. Sequential remains the
|
|
351
392
|
default. `parallel` and `mergeData` are invalid on a lone `code` or `file` call.
|
|
393
|
+
Budget overflow marks the batch failed and stops queued entries; already-running
|
|
394
|
+
entries settle and their completed commits remain. Parallel execution does not
|
|
395
|
+
multiply the aggregate output, log, or image allowance.
|
|
352
396
|
|
|
353
397
|
The outer deadline, host-call budget, log allowance, text budget and image limits
|
|
354
398
|
are shared across the batch. Individual read budgets are not reduced. Every
|
|
@@ -380,10 +424,13 @@ For long archive scans, use resumable chunks or a host background-job tool and w
|
|
|
380
424
|
progress records under `.work`. Shell commands inherit the current program
|
|
381
425
|
`timeoutMs` unless they specify their own; increasing the outer deadline no longer
|
|
382
426
|
leaves a hidden 60-second shell cap. Set the inner `bash` timeout shorter than the
|
|
383
|
-
outer program timeout (for example 10 seconds inside a 20-second program)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
427
|
+
outer program timeout (for example 10 seconds inside a 20-second program). The outer
|
|
428
|
+
deadline covers **all** waits and commands, including `sleep`; a shell's own `timeout`
|
|
429
|
+
command does not extend it. On a deadline or cancellation, the worker stops and
|
|
430
|
+
pending host calls get a bounded 250ms drain to retain owned-shell diagnostics and
|
|
431
|
+
finalize process termination. Non-cooperating host executors may still outlive that
|
|
432
|
+
drain. Cancellation is reported separately from timeout; neither triggers a retry.
|
|
433
|
+
Progress files survive shell execution but staged VFS writes may roll back.
|
|
387
434
|
|
|
388
435
|
Large returned objects are bounded previews, not retained artifacts. Select fields
|
|
389
436
|
and array windows before returning, rather than parsing a truncated preview.
|
|
@@ -432,6 +479,12 @@ continuation handles.
|
|
|
432
479
|
|
|
433
480
|
## Execution and automatic batching
|
|
434
481
|
|
|
482
|
+
Put already-known independent reads and checks in **one** Supernova program using
|
|
483
|
+
`Promise.all` (or `Promise.allSettled` when failures should remain independent).
|
|
484
|
+
Sequence edits and their known verification in that same program. Start another
|
|
485
|
+
invocation only when the returned evidence is needed to decide what to do next;
|
|
486
|
+
use focused read windows to keep the combined result within its output budget.
|
|
487
|
+
|
|
435
488
|
Compatible independently started reads coalesce at the worker/host boundary.
|
|
436
489
|
No additional batching command is required. Individual promises preserve their
|
|
437
490
|
values, errors and per-read budgets. File reads have bounded parallelism; writes,
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Shell commands preserve quoted executable paths. Parser failures suggest
|
|
10
|
+
literal argv for embedded scripts or a quoted heredoc, without rewriting or
|
|
11
|
+
automatically retrying commands.
|
|
12
|
+
- Invalid timeouts and null-byte arguments fail before flushing staged files.
|
|
13
|
+
Bounded command labels keep long scripts from crowding out diagnostics.
|
|
14
|
+
- Program deadlines retain pending shell output during bounded termination;
|
|
15
|
+
explicit cancellation is no longer misreported as a timeout.
|
|
16
|
+
- Parallel batches enforce shared output, log and image budgets instead of
|
|
17
|
+
reporting success after overflow. Queued entries stop, in-flight results and
|
|
18
|
+
completed commits remain, and aggregate logs stay capped.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Guidance explicitly batches known reads/checks and edits/verification in one
|
|
23
|
+
invocation, and states that the outer deadline includes all waits and commands.
|
|
24
|
+
|
|
25
|
+
### Internals
|
|
26
|
+
|
|
27
|
+
- 267 package tests (384 repository tests), 2,328 stress invocations, actual
|
|
28
|
+
Pi + OMP host checks, lint and both frozen token gates passed on macOS/Node 26.7.
|
|
29
|
+
|
|
5
30
|
## [0.8.1] - 2026-09-19
|
|
6
31
|
|
|
7
32
|
### Fixed
|
package/package.json
CHANGED
package/src/adapters/bash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { isString } from "../shared/decode.js";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeBash } from "../contract/bash.js";
|
|
4
4
|
import { sourceForReferences } from "../fs/source-window.js";
|
|
5
5
|
import { resolveWorkspacePath, runCommand, clearPathCache } from "../fs/workspace.js";
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ export function createBash(ctx) {
|
|
|
17
17
|
function bashCommand(params, literal) {
|
|
18
18
|
if (params?.command !== undefined && !isString(params.command)) throw new Error("bash command must be a string");
|
|
19
19
|
if (literal && (!isString(params.command) || params.args.some(arg => !isString(arg)))) throw new Error("bash argv requires a command string and an array of string args");
|
|
20
|
-
const command =
|
|
20
|
+
const command = String(params?.command ?? "");
|
|
21
21
|
|
|
22
22
|
if (!command.trim()) throw new Error("bash requires command");
|
|
23
23
|
|
|
@@ -32,6 +32,7 @@ export function createBash(ctx) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async function bash(params, signal) {
|
|
35
|
+
params = normalizeBash(params);
|
|
35
36
|
const cwd = getCwd();
|
|
36
37
|
const { literal, command, argv } = parseBash(params);
|
|
37
38
|
const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
@@ -49,7 +50,7 @@ export function createBash(ctx) {
|
|
|
49
50
|
res = await runCommand(argv, {
|
|
50
51
|
cwd: targetCwd,
|
|
51
52
|
env: hooks.commandEnv(),
|
|
52
|
-
commandLabel:
|
|
53
|
+
commandLabel: command,
|
|
53
54
|
timeoutMs: params?.timeoutMs === undefined ? config.timeoutMs : params.timeoutMs,
|
|
54
55
|
signal,
|
|
55
56
|
maxOutputChars: config.maxCallResultChars,
|
|
@@ -67,7 +68,12 @@ export function createBash(ctx) {
|
|
|
67
68
|
const { stdout, stderr } = res;
|
|
68
69
|
let text = combineBashText(stdout, stderr);
|
|
69
70
|
|
|
70
|
-
if (res.exitCode !== 0)
|
|
71
|
+
if (res.exitCode !== 0) {
|
|
72
|
+
if (!literal && /\bbash: (?:-c: )?line \d+: (?:syntax error|unexpected EOF)/.test(stderr)) {
|
|
73
|
+
text += '\nhint: Bash could not parse the command. For embedded scripts use literal argv, e.g. bash({command:"python3",args:["-c",data.script]}), or a quoted heredoc for shell pipelines. Do not blindly retry: earlier commands may have run.';
|
|
74
|
+
}
|
|
75
|
+
text += await sourceForReferences(cwd, targetCwd, text, signal, ledger);
|
|
76
|
+
}
|
|
71
77
|
|
|
72
78
|
return {
|
|
73
79
|
content: [{ type: "text", text }],
|
package/src/contract/bash.js
CHANGED
|
@@ -17,7 +17,10 @@ function normalizeArgv(args) {
|
|
|
17
17
|
throw new Error(`${ARGV_ERROR}; args[${i}] is ${type}; check the supplied data fields and pass each argument as a string`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
args.args = args.args.map(
|
|
20
|
+
args.args = args.args.map((arg, i) => {
|
|
21
|
+
if (arg.includes("\0")) throw new Error(`bash args[${i}] must not contain null bytes`);
|
|
22
|
+
return String(arg);
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
if (process.platform === "win32") {
|
|
23
26
|
delete args._directArgv;
|
|
@@ -37,9 +40,17 @@ function assertBashOptions(args) {
|
|
|
37
40
|
export function normalizeBash(command, opts) {
|
|
38
41
|
const args = isObject(command) ? { ...opts, ...command } : { command, ...opts };
|
|
39
42
|
assertBashOptions(args);
|
|
43
|
+
if (!isString(args.command) || !args.command.trim()) throw new Error("bash requires a non-empty command string");
|
|
44
|
+
if (args.command.includes("\0")) throw new Error("bash command must not contain null bytes");
|
|
40
45
|
normalizeArgv(args);
|
|
41
46
|
|
|
42
47
|
if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
|
|
48
|
+
// Reject before the host's external-mutation barrier can flush staged files.
|
|
49
|
+
if (args.timeoutMs !== undefined) {
|
|
50
|
+
const timeout = Number(args.timeoutMs);
|
|
51
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("command timeoutMs must be a positive finite number");
|
|
52
|
+
args.timeoutMs = Math.max(1, Math.min(2_147_483_647, Math.floor(timeout)));
|
|
53
|
+
}
|
|
43
54
|
|
|
44
55
|
return args;
|
|
45
56
|
}
|
package/src/fs/text-ops.js
CHANGED
|
@@ -81,21 +81,6 @@ export function resultDiff(response) {
|
|
|
81
81
|
return isObject(details) ? details.diff : undefined;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** Unwrap a single matching quote pair around the whole string (`'git status'`). */
|
|
85
|
-
export function unwrapIfFullyQuoted(s) {
|
|
86
|
-
if (s.length < 2) return s;
|
|
87
|
-
const q = s[0];
|
|
88
|
-
|
|
89
|
-
if (q !== "'" && q !== '"') return s;
|
|
90
|
-
|
|
91
|
-
if (s[s.length - 1] !== q) return s;
|
|
92
|
-
const inner = s.slice(1, -1);
|
|
93
|
-
|
|
94
|
-
if (inner.includes(q)) return s;
|
|
95
|
-
|
|
96
|
-
return inner;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
84
|
function totalContentLines(text) {
|
|
100
85
|
if (text === "") return 1;
|
|
101
86
|
|
package/src/fs/workspace.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { constants } from "node:os";
|
|
5
5
|
import { isString } from "../shared/decode.js";
|
|
6
|
+
import { truncateChars } from "../output/format.js";
|
|
6
7
|
|
|
7
8
|
let cachedCwd = null;
|
|
8
9
|
|
|
@@ -211,7 +212,10 @@ function attachCommandIO(state, options, argv, timeoutMs) {
|
|
|
211
212
|
clearTimeout(state.escalation);
|
|
212
213
|
options.signal?.removeEventListener("abort", onAbort);
|
|
213
214
|
};
|
|
214
|
-
state.timer = setTimeout(() => terminateCommand(state, new Error(
|
|
215
|
+
state.timer = setTimeout(() => terminateCommand(state, new Error(
|
|
216
|
+
"command timed out after " + timeoutMs + "ms: " + truncateChars(options.commandLabel ?? argv.join(" "), 240, "command").text
|
|
217
|
+
+ "\nhint: Increase this bash timeoutMs and the outer supernova timeoutMs, or split the work. Sleeps and every command in a shell chain share the same limit."
|
|
218
|
+
)), timeoutMs);
|
|
215
219
|
child.stdout.setEncoding("utf8");
|
|
216
220
|
child.stderr.setEncoding("utf8");
|
|
217
221
|
child.stdout.on("data", chunk => { state.stdout = appendCommandOutput(state, state.stdout, chunk); });
|
|
@@ -256,7 +256,7 @@ function formatBashFailure(command, res) {
|
|
|
256
256
|
const output = String(res.value).trimEnd();
|
|
257
257
|
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
258
258
|
|
|
259
|
-
return "command failed" + suffix + ": " + command + (output ? "\n" + output : "");
|
|
259
|
+
return "command failed" + suffix + ": " + truncateChars(command, 240, "command").text + (output ? "\n" + output : "");
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
function markTruncatedOutput(res, text) {
|
|
@@ -156,16 +156,35 @@ class ProgramBatch {
|
|
|
156
156
|
this.collectImages(result, i);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
parallelBudgetStop(settled) {
|
|
160
|
+
const results = settled.filter(Boolean);
|
|
161
|
+
let images = 0, bytes = 0, labelChars = 0;
|
|
162
|
+
for (const [i, result] of settled.entries()) {
|
|
163
|
+
let imageSeq = 0;
|
|
164
|
+
for (const block of result?.content ?? []) if (block.type === "image" && isString(block.data)) {
|
|
165
|
+
images++;
|
|
166
|
+
bytes += Buffer.byteLength(block.data, "base64");
|
|
167
|
+
labelChars += ("program " + (i + 1) + " image " + (++imageSeq)).length + 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let kind;
|
|
171
|
+
if (images > 16 || bytes > 20 * 1024 * 1024) kind = "image";
|
|
172
|
+
else if (results.some(result => result.details?.returnTruncated) || programBatchText(results, this.programs.length).length + labelChars > this.config.maxReturnChars) kind = "output";
|
|
173
|
+
else if (results.some(result => result.details?.logTruncated) || results.reduce((n, result) => n + (result.details?.logs?.length ?? 0), 0) > (this.config.maxLogLines ?? 100)) kind = "log";
|
|
174
|
+
return kind ? "batch " + kind + " budget exceeded; completed commits remain" : "";
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
async runParallel() {
|
|
160
178
|
const limit = Math.min(this.programs.length, MAX_PARALLEL_PROGRAMS);
|
|
161
179
|
const settled = Array.from({ length: this.programs.length });
|
|
162
180
|
let next = 0;
|
|
163
181
|
|
|
164
182
|
await Promise.all(Array.from({length: limit}, async () => {
|
|
165
|
-
while (next < this.programs.length && !this.combined.aborted && performance.now() < this.deadline) {
|
|
183
|
+
while (next < this.programs.length && !this.stopped && !this.combined.aborted && performance.now() < this.deadline) {
|
|
166
184
|
const i = next++;
|
|
167
185
|
settled[i] = await this.runOne(this.programs[i], i);
|
|
168
186
|
this.live[i] = [];
|
|
187
|
+
this.stopped ||= this.parallelBudgetStop(settled);
|
|
169
188
|
}
|
|
170
189
|
}));
|
|
171
190
|
|
|
@@ -174,7 +193,7 @@ class ProgramBatch {
|
|
|
174
193
|
this.takeSettled(settled[i], i);
|
|
175
194
|
}
|
|
176
195
|
|
|
177
|
-
if (settled.includes(undefined) || this.combined.aborted || performance.now() >= this.deadline) this.stopped = "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
196
|
+
if ((!this.stopped && settled.includes(undefined)) || this.combined.aborted || performance.now() >= this.deadline) this.stopped = "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
sequentialStop(result, i) {
|
|
@@ -222,6 +241,8 @@ class ProgramBatch {
|
|
|
222
241
|
const failed = this.results.filter(result => result.details?.ok === false).length;
|
|
223
242
|
const bounded = this.boundedText(failed);
|
|
224
243
|
const content = [{type:"text",text:bounded.text}];
|
|
244
|
+
const logs = this.results.flatMap(result => result.details?.logs ?? []);
|
|
245
|
+
const logLimit = this.config.maxLogLines ?? 100;
|
|
225
246
|
this.images.forEach((image,i) => content.push({type:"text",text:this.imageLabels[i]},image));
|
|
226
247
|
|
|
227
248
|
// Return a typed stop report instead of throwing away earlier results/images.
|
|
@@ -230,7 +251,7 @@ class ProgramBatch {
|
|
|
230
251
|
programs:this.results,attempted:this.results.length,total:this.programs.length,stopped:this.stopped,parallel:this.parallel,
|
|
231
252
|
result:bounded.truncated ? bounded.text : this.results.map(result=>result.details?.result),
|
|
232
253
|
returnTruncated:bounded.truncated || this.results.some(result=>result.details?.returnTruncated),
|
|
233
|
-
logTruncated:this.results.some(result=>result.details?.logTruncated),logs:
|
|
254
|
+
logTruncated:logs.length > logLimit || this.results.some(result=>result.details?.logTruncated),logs:logs.slice(0,logLimit),trace:this.trace,mutations:mutationTotals(this.results)}};
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
async run() {
|
package/src/runtime/reference.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Standing tool description: sent on every request. No result or history compression.
|
|
2
|
-
export const REFERENCE = `JS body/async arrow
|
|
3
|
-
read(path|paths,offset=1,limit?) → raw text/text[]; directories → entries; images: PNG/JPEG/GIF/WebP (≤16
|
|
4
|
-
Path-only
|
|
5
|
-
read({path,json:selector}) → parsed JSON
|
|
2
|
+
export const REFERENCE = `JS body/async arrow: read/write/edit/bash; no fs/import/require. file: workspace scripts; data: literals (≤48000 JSON chars).
|
|
3
|
+
read(path|paths,offset=1,limit?) → raw text/text[]; directories → entries; images: PNG/JPEG/GIF/WebP (≤16 images/20 MiB).
|
|
4
|
+
Path-only ≤160 lines AND 8192 characters (UTF-16); larger: read(path,{offset:1,limit:80}), about, or complete:true (whole file ≤31744 chars). Large JSONL: bounded bash parser.
|
|
5
|
+
read({path,json:selector}) → parsed JSON: ".field", ".a[0:3]", ".a.length", quoted keys, true; ≤16 MiB, no jq. {status:"too_large",keys|length} → narrow selector.
|
|
6
6
|
read("symbol or question") = read({query,resolve:true}) → view; check status; view.text is a span, not the file.
|
|
7
7
|
read(path,{about}) → windows; read({query,evidence:true}) → ranked evidence; read({path,outline:true}) → declarations.
|
|
8
8
|
write(path,text) replaces unread workspace files; write({path,content,append:true}) appends without reading. After read: edit or replace:true.
|
|
9
|
-
edit(path,oldText,newText) | edit({path,edits:[{oldText,newText}]}) exact
|
|
10
|
-
edit(view,text) replaces
|
|
11
|
-
bash(command,{cwd?,timeoutMs?}) | bash({command,args}) literal argv; bounded output, nonzero throws; inherits
|
|
9
|
+
edit(path,oldText,newText) | edit({path,edits:[{oldText,newText}]}) unique exact read text; returns numbered windows/checks/references.
|
|
10
|
+
edit(view,text) replaces span; edit(view,old,new) uniquely matches within it. edit(async()=>{...}) checkpoint: merge on success, rollback/rethrow on failure.
|
|
11
|
+
bash(command,{cwd?,timeoutMs?}) | bash({command,args}) literal argv for scripts; bounded output, nonzero throws. Outer timeoutMs caps ALL waits/commands; bash inherits unless overridden.
|
|
12
12
|
Edits stage until success; bash commits first. Array errors abort; Promise.allSettled for optional reads.
|
|
13
|
-
programs:[{code?,file?,data?}] inherits
|
|
14
|
-
Fresh guests/separate commits; sequential failure stops, prior commits stay. parallel:true for disjoint entries. Batch known
|
|
13
|
+
programs:[{code?,file?,data?}] inherits code OR file and data. Entries override source/data; mergeData:true shallow-merges objects (entry keys win).
|
|
14
|
+
Fresh guests/separate commits; sequential failure stops, prior commits stay. parallel:true for disjoint entries. Batch known reads/checks (Promise.all) + edits/verification in ONE call; split for new decisions.
|
|
15
15
|
`;
|
package/src/runtime/runtime.js
CHANGED
|
@@ -10,7 +10,8 @@ import { errorContext } from "../shared/syntax-context.js";
|
|
|
10
10
|
|
|
11
11
|
const WORKER_URL = new URL("./guest-worker.js", import.meta.url);
|
|
12
12
|
|
|
13
|
-
const ABORT_MESSAGE = "supernova
|
|
13
|
+
const ABORT_MESSAGE = "supernova aborted";
|
|
14
|
+
const TIMEOUT_MESSAGE = "supernova timed out: increase the outer timeoutMs (and any shorter bash timeoutMs), or split the program; sleeps count toward the deadline";
|
|
14
15
|
|
|
15
16
|
const MEMORY_POLL_MS = 50;
|
|
16
17
|
|
|
@@ -281,6 +282,7 @@ class GuestRun {
|
|
|
281
282
|
this.hostError = undefined;
|
|
282
283
|
this.notifyingHost = false;
|
|
283
284
|
this.aborting = false;
|
|
285
|
+
this.abortOutcome = undefined;
|
|
284
286
|
this.pending = new Set();
|
|
285
287
|
this.inputController = new AbortController();
|
|
286
288
|
this.rpcCount = 0;
|
|
@@ -320,15 +322,18 @@ class GuestRun {
|
|
|
320
322
|
try { this.nova.cancel?.(); } catch {} finally { this.notifyingHost = false; }
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
abort() {
|
|
325
|
+
abort(timedOut = false) {
|
|
324
326
|
if (this.finished || this.aborting) return;
|
|
325
327
|
this.aborting = true;
|
|
328
|
+
this.accepting = false;
|
|
329
|
+
this.abortOutcome = this.fail((timedOut ? TIMEOUT_MESSAGE : ABORT_MESSAGE) + " (ran " + this.wall() + "ms of " + this.timeoutMs + "ms)");
|
|
326
330
|
this.cancelHost();
|
|
327
331
|
|
|
328
|
-
try { this.onTimeout?.(); } catch {}
|
|
332
|
+
if (timedOut) { try { this.onTimeout?.(); } catch {} }
|
|
329
333
|
|
|
330
|
-
|
|
331
|
-
|
|
334
|
+
// Stop the guest immediately, then use the bounded host drain to retain
|
|
335
|
+
// shell diagnostics and wait for process-tree termination before returning.
|
|
336
|
+
void this.complete(this.abortOutcome);
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
postResult(message) {
|
|
@@ -365,6 +370,10 @@ class GuestRun {
|
|
|
365
370
|
void killWorker(this.handle);
|
|
366
371
|
await this.drainPending(outcome);
|
|
367
372
|
if (this.finished) return;
|
|
373
|
+
if (this.abortOutcome) {
|
|
374
|
+
const diagnostic = this.hostError && this.hostError !== "aborted" ? "\n" + this.hostError : "";
|
|
375
|
+
outcome = this.fail(this.abortOutcome.error + diagnostic);
|
|
376
|
+
}
|
|
368
377
|
this.finish(outcome.ok && this.hostError ? this.fail(this.hostError) : outcome);
|
|
369
378
|
}
|
|
370
379
|
|
|
@@ -448,7 +457,7 @@ class GuestRun {
|
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
async attachWorker() {
|
|
451
|
-
if (this.wall() >= this.timeoutMs) { this.abort(); return false; }
|
|
460
|
+
if (this.wall() >= this.timeoutMs) { this.abort(true); return false; }
|
|
452
461
|
this.handle = acquireWorker(this.config);
|
|
453
462
|
await this.handle.ready;
|
|
454
463
|
if (this.finished || this.signal?.aborted) { this.abort(); return false; }
|
|
@@ -459,7 +468,7 @@ class GuestRun {
|
|
|
459
468
|
this.handle.worker.on("message", this.onMessage);
|
|
460
469
|
this.handle.worker.on("error", this.onError);
|
|
461
470
|
this.handle.worker.on("exit", this.onExit);
|
|
462
|
-
if (this.wall() >= this.timeoutMs) { this.abort(); return false; }
|
|
471
|
+
if (this.wall() >= this.timeoutMs) { this.abort(true); return false; }
|
|
463
472
|
this.available = available;
|
|
464
473
|
|
|
465
474
|
return true;
|
|
@@ -488,7 +497,7 @@ class GuestRun {
|
|
|
488
497
|
start() {
|
|
489
498
|
return new Promise((resolve) => {
|
|
490
499
|
this.resolve = resolve;
|
|
491
|
-
this.timer = setTimeout(() => this.abort(), Math.min(this.timeoutMs, 2147483647));
|
|
500
|
+
this.timer = setTimeout(() => this.abort(true), Math.min(this.timeoutMs, 2147483647));
|
|
492
501
|
this.memTimer = setInterval(() => {
|
|
493
502
|
const now = rssBytes();
|
|
494
503
|
|