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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +104 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +289 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. 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.5.0` excludes `0.6.0`). After 0.6.0 is
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.6.0
51
- omp install npm:pi-supernova@0.6.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
@@ -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 631 tokens with o200k_base and 626 with
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,499 | **66.28%** | 6,461 / 1,043 | 2,151 |
121
- | cl100k_base | 14,649 | 5,197 | **64.52%** | 5,684 / 945 | 2,055 |
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 new standing guidance adds 13 definition tokens per
131
- request (618 to 631 / 613 to 626), and that cost is included in the after totals.
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}${outcome.mutations?.committed || outcome.mutations?.rolledBack || outcome.mutations?.external ? mutationText(outcome) : ""}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
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 = new AbortController();
236
- const abortRun = () => runController.abort(signal?.reason);
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
- refreshCatalog(runBridge);
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.setCallListener(null);
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 response;
375
+ return packExecuteResult(outcome, call, runBridge, budget, runOpts, peakSeen);
335
376
  },
336
377
  });
337
378
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CodeMode for Pi and OMP: read, edit, write and bash, with transactional files, source views and shared-input program batches.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
@@ -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
+ }