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
package/README.md
CHANGED
|
@@ -12,6 +12,24 @@ 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
|
+
## Unreleased
|
|
16
|
+
|
|
17
|
+
- **`parallel: true` on `programs`:** independent entries run at once (up to 8),
|
|
18
|
+
keep result order, and do not stop siblings on failure. Sequential is still
|
|
19
|
+
the default.
|
|
20
|
+
- **JSON `.length`:** `read({path, json:".items.length"})` returns the array
|
|
21
|
+
length without dumping the array.
|
|
22
|
+
- Prefer `edit` for a file you already read; `write` still replaces the file.
|
|
23
|
+
|
|
24
|
+
## What is new in 0.7.0
|
|
25
|
+
|
|
26
|
+
- **Faster batches:** guest workers pipeline their successor, so sequential
|
|
27
|
+
programs run about twice as fast with no token cost.
|
|
28
|
+
- **Hardened guest:** `process.kill` is sealed out; patch hunks report
|
|
29
|
+
relocation and ambiguous hunks fail instead of misapplying.
|
|
30
|
+
- **Leaner receipts:** multi-edit output caps at 32 matches with exact totals,
|
|
31
|
+
write receipts go workspace-relative, evidence/outline reads go compact.
|
|
32
|
+
|
|
15
33
|
## What is new in 0.6.0
|
|
16
34
|
|
|
17
35
|
- **Shared batch input:** supply top-level `data` once; each program gets an
|
|
@@ -43,12 +61,12 @@ pi install /path/to/pi-stack/packages/pi-supernova
|
|
|
43
61
|
|
|
44
62
|
Git pushes do not update npm installations. Publish the new npm version first;
|
|
45
63
|
then reinstall it in the host. Reinstall explicitly when an existing version
|
|
46
|
-
range excludes the new minor version (`^0.
|
|
64
|
+
range excludes the new minor version (`^0.6.0` excludes `0.7.0`). After 0.7.0 is
|
|
47
65
|
published, pin that release with:
|
|
48
66
|
|
|
49
67
|
```bash
|
|
50
|
-
pi install npm:pi-supernova@0.
|
|
51
|
-
omp install npm:pi-supernova@0.
|
|
68
|
+
pi install npm:pi-supernova@0.7.0
|
|
69
|
+
omp install npm:pi-supernova@0.7.0
|
|
52
70
|
```
|
|
53
71
|
|
|
54
72
|
In Pi, `pi list` shows the configured package sources. A local path uses that
|
|
@@ -268,6 +286,12 @@ results/images: isError and details.ok identify failure, details.programs contai
|
|
|
268
286
|
every attempted result, and details.attempted/total identifies unstarted work.
|
|
269
287
|
Single code/file invocations retain their existing throwing behavior.
|
|
270
288
|
|
|
289
|
+
Set `parallel: true` with `programs` to run independent entries concurrently
|
|
290
|
+
(up to 8 at once). Each still gets a fresh guest and its own commit; results stay
|
|
291
|
+
in submission order. A failed entry does not stop siblings. Two entries writing
|
|
292
|
+
the same file race: the losing commit reports a conflict. Sequential remains the
|
|
293
|
+
default. `parallel` is invalid on a lone `code` or `file` call.
|
|
294
|
+
|
|
271
295
|
The outer deadline, host-call budget, log allowance, text budget and image limits
|
|
272
296
|
are shared across the batch. Individual read budgets are not reduced. Every
|
|
273
297
|
attempted program's original text is returned in length-delimited blocks; ordinary
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## [0.7.0] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Internals
|
|
8
|
+
|
|
9
|
+
- Shared `src/contract/` for read/edit/bash shapes. Guest and host classify once;
|
|
10
|
+
guest no longer reimplements exclusive-mode routing before RPC.
|
|
11
|
+
- Read dispatch is `classifyRead` → kind table. Disk vs staged `about` focus share
|
|
12
|
+
one `focusAbout` helper.
|
|
13
|
+
- Invoke permission/target resolution lives in `bridge/invoke.js`.
|
|
14
|
+
- Native adapters live in `src/adapters/{read,write,edit,bash,list}.js`;
|
|
15
|
+
`createNativeAdapters` is a 30-line assembler. The host kernel stays in
|
|
16
|
+
`host-bridge.js`.
|
|
17
|
+
- Line/edit helpers in `fs/text-ops.js`. Unused catalog search/describe APIs and
|
|
18
|
+
the unused native-tool registrar are gone.
|
|
19
|
+
- Per-function cyclomatic complexity is under 10: guest lifecycle is `GuestRun`,
|
|
20
|
+
program batches are `ProgramBatch`, snap ranking/read/edit/bash/VFS/evidence/
|
|
21
|
+
output/UI are extracted helpers and classify tables. Same public behavior;
|
|
22
|
+
BIND DAG unchanged.
|
|
23
|
+
- Guest isolate: no `fs` / `child_process` / `import` / `require`. After `read()`
|
|
24
|
+
of a path, `write()` of that path throws (use `edit`, or `replace:true`).
|
|
25
|
+
Raw reads of large source require `about` / offset or `complete:true`;
|
|
26
|
+
large JSON returns a shape routing value (top-level keys, or length for
|
|
27
|
+
arrays) instead of throwing.
|
|
28
|
+
- Standing reference stays one nova invocation: prefer `edit` after `read`,
|
|
29
|
+
`json`/about/`complete:true` for large files, no guest `fs`, and
|
|
30
|
+
`programs`/`parallel:true` for independent work *inside* one call. Overlapping
|
|
31
|
+
sibling `supernova` calls still hint that independent work belongs in one
|
|
32
|
+
program.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `parallel: true` on `programs`: independent entries run concurrently (up to 8
|
|
37
|
+
lanes) in fresh guests, results keep submission order, and a failed entry does
|
|
38
|
+
not stop siblings. Two entries writing the same file race; the losing commit
|
|
39
|
+
reports a conflict. Sequential batches still stop on the first failure.
|
|
40
|
+
`parallel` is rejected on a lone `code` or `file` call.
|
|
41
|
+
- JSON selectors accept array `.length` (for example `.items.length`) so a
|
|
42
|
+
catalog count does not require dumping the array. String `.length` is still
|
|
43
|
+
rejected; this is not jq.
|
|
44
|
+
- Raw reads of JSON above the bound return a shape routing value instead of
|
|
45
|
+
throwing: `{status:"too_large", path, chars, keys}` for objects (`length`
|
|
46
|
+
for top-level arrays), so the same program can project with `json:".field"`
|
|
47
|
+
and array reads survive one oversize member. Malformed JSON still throws;
|
|
48
|
+
non-JSON text still returns verbatim.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- Prefer `edit` for a file already read; `write` remains replace-the-file.
|
|
53
|
+
- Overlapping sibling `supernova` calls hint to batch independent work as
|
|
54
|
+
`programs` with `parallel:true`.
|
|
55
|
+
- Empty programs (no adapter calls) still draw a nova card with a result preview
|
|
56
|
+
instead of a one-line `complete` status.
|
|
57
|
+
- Shorter standing tool reference: 68 fewer tokens per request with the same
|
|
58
|
+
commands and surface needles. Wording compression plus dropped peripheral
|
|
59
|
+
clauses; the JSON/about signatures, prefer-edit rule, and batching nudge
|
|
60
|
+
stay. Ablation family verified live over 12 green gpt-6-astra runs.
|
|
61
|
+
- Standing tool reference trimmed by 120 more tokens per request (o200k_base),
|
|
62
|
+
below the previous release baseline: limits and failure patterns already
|
|
63
|
+
taught by engine errors are no longer repeated proactively, and the batching
|
|
64
|
+
nudge, guest-confinement rule, and checkpoint clause are compressed. All
|
|
65
|
+
surface needles, the prefer-edit rule, and the oversize-JSON routing line
|
|
66
|
+
stay. Live-verified over 2 green gpt-6-astra runs against 2 task-matched
|
|
67
|
+
controls with no strategy change.
|
|
68
|
+
- An acquired guest worker pipelines its successor while the run executes, so
|
|
69
|
+
back-to-back programs share construction cost: sequential batches run ~2x
|
|
70
|
+
faster (22.4ms to 10.5ms per realistic program). Isolated cold starts are
|
|
71
|
+
unchanged apart from the deferred successor spawn.
|
|
72
|
+
- Multi-edit receipts render the first 32 matches with exact totals and an
|
|
73
|
+
`…N more matches` note instead of unbounded lines.
|
|
74
|
+
- Write receipts report workspace-relative paths, matching `edited <rel>`.
|
|
75
|
+
- Evidence and outline reads return compact JSON instead of pretty-printed.
|
|
76
|
+
- Refused bridge calls (unknown or excluded tools) no longer consume the host
|
|
77
|
+
call budget; routing validation runs before charging.
|
|
78
|
+
- The VFS body cache is gone: every read hits disk or its overlay, and CAS
|
|
79
|
+
baselines are the only retained per-file state.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- `bash({command, args}, opts)` no longer drops the second options argument:
|
|
84
|
+
`cwd` (and `timeoutMs`) merge in, with the params object's own keys winning
|
|
85
|
+
on conflict. Covered by argv-form tests.
|
|
86
|
+
- Over-budget JSON selections return an in-band routing value
|
|
87
|
+
(`{status:"too_large", path, selector, chars}` with `keys` or `length`)
|
|
88
|
+
instead of throwing, so one oversize field no longer kills the read and
|
|
89
|
+
small sibling selections flow through. The standing reference line covers
|
|
90
|
+
raw and selection routing at the same token cost; live-verified over 3
|
|
91
|
+
green runs (top-level keys now answer in 1 call instead of 2).
|
|
92
|
+
- Memory-limit failures now report the RSS growth, the in-flight operation,
|
|
93
|
+
host-call count, and tracked host bytes (index entries, overlays),
|
|
94
|
+
splitting tracked from untracked growth so host-side pressure
|
|
95
|
+
is distinguishable from tool-side growth.
|
|
96
|
+
- `process.kill` is sealed out of the guest realm: signals are process-wide
|
|
97
|
+
and could terminate the host. Stop processes with `bash`.
|
|
98
|
+
- Patch hunks that drift report their relocation per hunk, and a hunk whose
|
|
99
|
+
context matches more than one location fails with a disambiguation error
|
|
100
|
+
instead of applying at the first candidate.
|
|
101
|
+
- A failed commit keeps CAS baselines for files it never touched, so a later
|
|
102
|
+
write to a diverged path fails loudly instead of re-capturing unknown
|
|
103
|
+
bytes as the new truth.
|
|
104
|
+
- The workspace index reuses one scratch read buffer instead of allocating
|
|
105
|
+
512 KiB per file, bounding transient RSS on large-tree scans.
|
|
106
|
+
|
|
3
107
|
## [0.6.0] - 2026-09-15
|
|
4
108
|
|
|
5
109
|
### Added
|
package/docs/TOKEN_COSTS.md
CHANGED
|
@@ -86,7 +86,7 @@ as the original workload: removing a decision boundary cannot satisfy this gate.
|
|
|
86
86
|
|
|
87
87
|
### Definition and result accounting
|
|
88
88
|
|
|
89
|
-
The current serialized definition is
|
|
89
|
+
The current serialized definition is 602 tokens with o200k_base and 595 with
|
|
90
90
|
cl100k_base, versus 908 and 901 in the frozen non-batched baseline. It retains
|
|
91
91
|
command signatures, complete-read and JSON limits, array-read failure rules,
|
|
92
92
|
transaction boundaries, batch defaults and edit/view guidance on every request.
|
|
@@ -117,8 +117,8 @@ this is not a comparison against a hypothetical request that could never run.
|
|
|
117
117
|
|
|
118
118
|
| Tokenizer | Repeated-input traffic | Shared-input traffic | Reduction | Arguments before / after | Unchanged result tokens |
|
|
119
119
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
120
|
-
| o200k_base | 16,309 | 5,
|
|
121
|
-
| cl100k_base | 14,649 | 5,
|
|
120
|
+
| o200k_base | 16,309 | 5,441 | **66.64%** | 6,461 / 1,043 | 2,151 |
|
|
121
|
+
| cl100k_base | 14,649 | 5,135 | **64.95%** | 5,684 / 945 | 2,055 |
|
|
122
122
|
|
|
123
123
|
Each arm has one tool invocation followed by the final answer request:
|
|
124
124
|
|
|
@@ -127,8 +127,9 @@ Total = 2*D + 2*A + R
|
|
|
127
127
|
~~~
|
|
128
128
|
|
|
129
129
|
Arguments are charged when generated and when replayed; the complete result is
|
|
130
|
-
charged on handoff. The
|
|
131
|
-
request
|
|
130
|
+
charged on handoff. The standing guidance measures 602 definition tokens per
|
|
131
|
+
request with o200k_base (595 with cl100k_base), and that cost is included in
|
|
132
|
+
the after totals.
|
|
132
133
|
There is no source compression, result elision, hidden output or lost decision
|
|
133
134
|
boundary. Programs receive fresh data copies, not a shared mutable heap.
|
|
134
135
|
|
|
@@ -164,6 +165,13 @@ untouched; programs, arguments, failures and decision boundaries are unchanged.
|
|
|
164
165
|
The shared-input comparison separately requires equal complete result text in
|
|
165
166
|
both arms, after only run-metadata normalization.
|
|
166
167
|
|
|
168
|
+
Contract v3 reports write receipts relative to the workspace (`wrote rel/path`,
|
|
169
|
+
matching the long-standing `edited <rel>` form) instead of absolute paths. The
|
|
170
|
+
runner strips the frozen `/workspace/` prefix from baseline outputs; the
|
|
171
|
+
historical fixture and its traffic counts stay untouched. Temporary workspace
|
|
172
|
+
prefixes no longer appear in live receipts, so that normalization only applies
|
|
173
|
+
to the frozen baseline side.
|
|
174
|
+
|
|
167
175
|
The README and these docs ship in the npm tarball. Benchmarks and test fixtures
|
|
168
176
|
remain in the GitHub checkout, so their links above use GitHub URLs.
|
|
169
177
|
|
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ try {
|
|
|
25
25
|
Array: (items, opts) => ({ type: "array", items, ...opts }),
|
|
26
26
|
Integer: (opts) => ({ type: "integer", ...opts }),
|
|
27
27
|
Optional: (s) => ({ ...s }),
|
|
28
|
+
Boolean: (opts) => ({ type: "boolean", ...opts }),
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -127,8 +128,10 @@ error: ${outcome.error}${logsBlock(outcome)}`;
|
|
|
127
128
|
function successText(outcome, call) {
|
|
128
129
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
129
130
|
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
131
|
+
const m = outcome.mutations;
|
|
132
|
+
const showMutations = m && (m.committed || m.rolledBack || m.external || m.pendingCommits || m.recoveryFailed) ? mutationText(outcome) : "";
|
|
130
133
|
|
|
131
|
-
return `ok #${call} ${outcome.wallMs}ms${truncated}${
|
|
134
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${showMutations}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
function fitOutput(outcome, call, limit, format) {
|
|
@@ -200,12 +203,119 @@ export function registerCodeMode(pi) {
|
|
|
200
203
|
speculateCommit: () => runBridge.barrier(() => runBridge.commitSpeculation()),
|
|
201
204
|
speculateRollback: () => runBridge.barrier(() => runBridge.rollbackSpeculation()),
|
|
202
205
|
names: () => ["read", "edit", "write", "bash"],
|
|
206
|
+
describeMemory: () => runBridge.describeMemory?.() ?? null,
|
|
203
207
|
batchRead: runBridge.supportsBatchRead(),
|
|
204
208
|
nativeArgv: runBridge.supportsNativeArgv?.() === true,
|
|
205
209
|
cancel,
|
|
206
210
|
};
|
|
207
211
|
}
|
|
208
212
|
|
|
213
|
+
function rejectLoneParallel(params) {
|
|
214
|
+
if (params?.parallel !== undefined) throw new Error("parallel applies to the programs array; no commands ran");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function bindRunSignal(signal) {
|
|
218
|
+
const runController = new AbortController();
|
|
219
|
+
const abortRun = () => runController.abort(signal?.reason);
|
|
220
|
+
|
|
221
|
+
if (signal?.aborted) abortRun();
|
|
222
|
+
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
223
|
+
|
|
224
|
+
return { runController, abortRun };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function openRunBridge(ctx, runCwd, budget, runController) {
|
|
228
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
|
|
229
|
+
runBridge.bindCallContext(ctx, runController.signal);
|
|
230
|
+
runBridge.resetCallBudget();
|
|
231
|
+
|
|
232
|
+
return runBridge;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runAndCommit(params, runCwd, runBridge, abortRun, runController, budget) {
|
|
236
|
+
refreshCatalog(runBridge);
|
|
237
|
+
runBridge.beginSpeculation();
|
|
238
|
+
const outcome = await runGuestProgram({
|
|
239
|
+
code: params?.code,
|
|
240
|
+
file: params?.file,
|
|
241
|
+
cwd: runCwd,
|
|
242
|
+
data: params?.data,
|
|
243
|
+
nova: makeNovaApi(runBridge, abortRun),
|
|
244
|
+
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: params?.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs) },
|
|
245
|
+
signal: runController.signal,
|
|
246
|
+
onTimeout: abortRun,
|
|
247
|
+
});
|
|
248
|
+
runBridge.close();
|
|
249
|
+
|
|
250
|
+
if (outcome.ok) {
|
|
251
|
+
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
252
|
+
await runBridge.commitSpeculation();
|
|
253
|
+
}
|
|
254
|
+
else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
255
|
+
|
|
256
|
+
return outcome;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scheduleWarm(runController) {
|
|
260
|
+
cancelWarmTimer();
|
|
261
|
+
|
|
262
|
+
if (!stopped && !runController.signal.aborted) {
|
|
263
|
+
// Deliver the result before paying for another Worker constructor.
|
|
264
|
+
warmTimer = setImmediate(() => {
|
|
265
|
+
warmTimer = undefined;
|
|
266
|
+
|
|
267
|
+
if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
|
|
268
|
+
});
|
|
269
|
+
warmTimer.unref?.();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function finishRun(runBridge, emitProgress, signal, abortRun, runController) {
|
|
274
|
+
runBridge.setCallListener(null);
|
|
275
|
+
emitProgress.flush();
|
|
276
|
+
signal?.removeEventListener("abort", abortRun);
|
|
277
|
+
// Prepare one pristine worker during the model's next decision. Never
|
|
278
|
+
// recycle a worker that has executed arbitrary guest JavaScript.
|
|
279
|
+
scheduleWarm(runController);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function attachReceipts(outcome, trace) {
|
|
283
|
+
if (outcome.ok && outcome.result === undefined) {
|
|
284
|
+
const receipts = mutationReceipts(trace);
|
|
285
|
+
|
|
286
|
+
if (receipts) {
|
|
287
|
+
outcome.resultText = receipts;
|
|
288
|
+
outcome.undefinedReturn = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function throwIfFailed(outcome, visible, response) {
|
|
294
|
+
if (outcome.ok) return response;
|
|
295
|
+
const error = new Error(visible);
|
|
296
|
+
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function packExecuteResult(outcome, call, runBridge, budget, runOpts, peakSeen) {
|
|
301
|
+
if (budget) budget.logLines += outcome.logs?.length ?? 0;
|
|
302
|
+
outcome.overlappedTurn = !runOpts?.parallel && peakSeen > 1 ? peakSeen : 0;
|
|
303
|
+
outcome.mutations = runBridge.getMutations();
|
|
304
|
+
const trace = runBridge.getTrace();
|
|
305
|
+
attachReceipts(outcome, trace);
|
|
306
|
+
const bounded = fitOutput(outcome, call, config.maxReturnChars, outcome.ok ? successText : errorText);
|
|
307
|
+
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
308
|
+
const response = result(visible, {
|
|
309
|
+
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
310
|
+
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
311
|
+
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
315
|
+
|
|
316
|
+
return throwIfFailed(outcome, visible, response);
|
|
317
|
+
}
|
|
318
|
+
|
|
209
319
|
pi.registerTool({
|
|
210
320
|
name: "supernova",
|
|
211
321
|
label: "Supernova",
|
|
@@ -221,6 +331,7 @@ export function registerCodeMode(pi) {
|
|
|
221
331
|
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
222
332
|
data: Type.Optional(Type.Unknown()),
|
|
223
333
|
}, {additionalProperties:false}), {minItems:1,maxItems:32})),
|
|
334
|
+
parallel: Type.Optional(Type.Boolean()),
|
|
224
335
|
}),
|
|
225
336
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
226
337
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
@@ -228,50 +339,26 @@ export function registerCodeMode(pi) {
|
|
|
228
339
|
mergeCallAndResult: true,
|
|
229
340
|
renderCall: renderSupernovaCall,
|
|
230
341
|
renderResult: renderSupernovaResult,
|
|
231
|
-
execute: async function execute(_id, params, signal, onUpdate, ctx, budget) {
|
|
342
|
+
execute: async function execute(_id, params, signal, onUpdate, ctx, budget, runOpts) {
|
|
232
343
|
if (params?.programs !== undefined) return runProgramBatch(_id,params,signal,onUpdate,ctx,config,execute);
|
|
344
|
+
rejectLoneParallel(params);
|
|
233
345
|
cancelWarmTimer();
|
|
234
346
|
const runCwd = isString(ctx?.cwd) && ctx.cwd ? ctx.cwd : cwd;
|
|
235
|
-
const runController =
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (signal?.aborted) abortRun();
|
|
239
|
-
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
240
|
-
const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
|
|
241
|
-
runBridge.bindCallContext(ctx, runController.signal);
|
|
242
|
-
runBridge.resetCallBudget();
|
|
243
|
-
|
|
347
|
+
const { runController, abortRun } = bindRunSignal(signal);
|
|
348
|
+
const runBridge = openRunBridge(ctx, runCwd, budget, runController);
|
|
244
349
|
const call = ++programSeq;
|
|
245
350
|
runBridge.ledger.beginProgram(call);
|
|
246
351
|
const emitProgress = progressEmitter(onUpdate);
|
|
247
352
|
runBridge.setCallListener((_record, trace) => emitProgress(trace));
|
|
248
353
|
emitProgress([]);
|
|
249
354
|
const started = performance.now();
|
|
250
|
-
let outcome;
|
|
251
355
|
inFlight += 1;
|
|
252
356
|
overlapPeak = Math.max(overlapPeak, inFlight);
|
|
253
357
|
let peakSeen = overlapPeak;
|
|
358
|
+
let outcome;
|
|
254
359
|
|
|
255
360
|
try {
|
|
256
|
-
|
|
257
|
-
runBridge.beginSpeculation();
|
|
258
|
-
outcome = await runGuestProgram({
|
|
259
|
-
code: params?.code,
|
|
260
|
-
file: params?.file,
|
|
261
|
-
cwd: runCwd,
|
|
262
|
-
data: params?.data,
|
|
263
|
-
nova: makeNovaApi(runBridge, abortRun),
|
|
264
|
-
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: params?.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs) },
|
|
265
|
-
signal: runController.signal,
|
|
266
|
-
onTimeout: abortRun,
|
|
267
|
-
});
|
|
268
|
-
runBridge.close();
|
|
269
|
-
|
|
270
|
-
if (outcome.ok) {
|
|
271
|
-
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
272
|
-
await runBridge.commitSpeculation();
|
|
273
|
-
}
|
|
274
|
-
else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
361
|
+
outcome = await runAndCommit(params, runCwd, runBridge, abortRun, runController, budget);
|
|
275
362
|
} catch (error) {
|
|
276
363
|
abortRun();
|
|
277
364
|
runBridge.close();
|
|
@@ -282,56 +369,10 @@ export function registerCodeMode(pi) {
|
|
|
282
369
|
peakSeen = Math.max(peakSeen, overlapPeak);
|
|
283
370
|
inFlight -= 1;
|
|
284
371
|
if (inFlight === 0) overlapPeak = 0;
|
|
285
|
-
runBridge
|
|
286
|
-
emitProgress.flush();
|
|
287
|
-
signal?.removeEventListener("abort", abortRun);
|
|
288
|
-
// Prepare one pristine worker during the model's next decision. Never
|
|
289
|
-
// recycle a worker that has executed arbitrary guest JavaScript.
|
|
290
|
-
cancelWarmTimer();
|
|
291
|
-
|
|
292
|
-
if (!stopped && !runController.signal.aborted) {
|
|
293
|
-
// Deliver the result before paying for another Worker constructor.
|
|
294
|
-
warmTimer = setImmediate(() => {
|
|
295
|
-
warmTimer = undefined;
|
|
296
|
-
|
|
297
|
-
if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
|
|
298
|
-
});
|
|
299
|
-
warmTimer.unref?.();
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (budget) budget.logLines += outcome.logs?.length ?? 0;
|
|
304
|
-
outcome.overlappedTurn = peakSeen > 1 ? peakSeen : 0;
|
|
305
|
-
outcome.mutations = runBridge.getMutations();
|
|
306
|
-
const trace = runBridge.getTrace();
|
|
307
|
-
|
|
308
|
-
if (outcome.ok && outcome.result === undefined) {
|
|
309
|
-
const receipts = mutationReceipts(trace);
|
|
310
|
-
|
|
311
|
-
if (receipts) {
|
|
312
|
-
outcome.resultText = receipts;
|
|
313
|
-
outcome.undefinedReturn = false;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const format = outcome.ok ? successText : errorText;
|
|
317
|
-
const bounded = fitOutput(outcome, call, config.maxReturnChars, format);
|
|
318
|
-
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
319
|
-
|
|
320
|
-
const response = result(visible, {
|
|
321
|
-
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
322
|
-
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
323
|
-
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
327
|
-
|
|
328
|
-
if (!outcome.ok) {
|
|
329
|
-
const error = new Error(visible);
|
|
330
|
-
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
331
|
-
throw error;
|
|
372
|
+
finishRun(runBridge, emitProgress, signal, abortRun, runController);
|
|
332
373
|
}
|
|
333
374
|
|
|
334
|
-
return
|
|
375
|
+
return packExecuteResult(outcome, call, runBridge, budget, runOpts, peakSeen);
|
|
335
376
|
},
|
|
336
377
|
});
|
|
337
378
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isString } from "../shared/decode.js";
|
|
2
|
+
import { unwrapIfFullyQuoted } from "../fs/text-ops.js";
|
|
3
|
+
import { sourceForReferences } from "../fs/source-window.js";
|
|
4
|
+
import { resolveWorkspacePath, runCommand, clearPathCache } from "../fs/workspace.js";
|
|
5
|
+
|
|
6
|
+
export function createBash(ctx) {
|
|
7
|
+
const { getCwd, vfs, config, index, ledger, hooks } = ctx;
|
|
8
|
+
function combineBashText(stdout, stderr) {
|
|
9
|
+
return stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isLiteralArgv(params) {
|
|
13
|
+
return Array.isArray(params?.args) && process.platform !== "win32" && params.args.length === Object.keys(params.args).length && params.args.every(isString);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function bashCommand(params, literal) {
|
|
17
|
+
if (params?.command !== undefined && !isString(params.command)) throw new Error("bash command must be a string");
|
|
18
|
+
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");
|
|
19
|
+
const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
20
|
+
|
|
21
|
+
if (!command.trim()) throw new Error("bash requires command");
|
|
22
|
+
|
|
23
|
+
return command;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseBash(params) {
|
|
27
|
+
const literal = isLiteralArgv(params);
|
|
28
|
+
const command = bashCommand(params, literal);
|
|
29
|
+
|
|
30
|
+
return { literal, command, argv: literal ? [command, ...params.args] : ["bash", "-c", command] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function bash(params, signal) {
|
|
34
|
+
const cwd = getCwd();
|
|
35
|
+
const { literal, command, argv } = parseBash(params);
|
|
36
|
+
const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
37
|
+
|
|
38
|
+
const transactionBarrier = await vfs.prepareExternalMutation("bash");
|
|
39
|
+
let res;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
res = await runCommand(argv, {
|
|
43
|
+
cwd: targetCwd,
|
|
44
|
+
env: hooks.commandEnv(),
|
|
45
|
+
commandLabel: literal ? command : undefined,
|
|
46
|
+
timeoutMs: params?.timeoutMs,
|
|
47
|
+
signal,
|
|
48
|
+
maxOutputChars: config.maxCallResultChars,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message, signal, ledger);
|
|
52
|
+
throw error;
|
|
53
|
+
} finally {
|
|
54
|
+
vfs.invalidateObserved();
|
|
55
|
+
index.invalidate();
|
|
56
|
+
clearPathCache();
|
|
57
|
+
hooks.workspaceChanged();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { stdout, stderr } = res;
|
|
61
|
+
let text = combineBashText(stdout, stderr);
|
|
62
|
+
|
|
63
|
+
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text, signal, ledger);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text }],
|
|
67
|
+
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
68
|
+
isError: res.exitCode !== 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { bash };
|
|
73
|
+
}
|