pi-supernova 0.9.0 → 0.10.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 +191 -14
- package/index.js +30 -9
- package/package.json +1 -1
- package/src/adapters/bash.js +40 -0
- package/src/adapters/read.js +24 -3
- package/src/bridge/host-bridge.js +50 -7
- package/src/bridge/invoke.js +6 -1
- package/src/bridge/trace.js +4 -1
- package/src/context/evidence.js +10 -1
- package/src/context/fuzzy.js +116 -43
- package/src/context/query.js +14 -5
- package/src/context/repo-index.js +15 -4
- package/src/context/snap-search.js +14 -13
- package/src/context/snap.js +11 -2
- package/src/contract/bash.js +43 -11
- package/src/contract/read.js +19 -5
- package/src/fs/background.js +304 -0
- package/src/fs/commit.js +6 -0
- package/src/fs/file-io.js +17 -2
- package/src/fs/process-tree.js +92 -0
- package/src/fs/read-window.js +18 -1
- package/src/fs/vfs.js +17 -4
- package/src/fs/workspace.js +51 -46
- package/src/output/bottleneck.js +14 -1
- package/src/runtime/guest-api.js +36 -20
- package/src/runtime/reference.js +11 -14
- package/src/shared/syntax-context.js +28 -7
package/README.md
CHANGED
|
@@ -12,6 +12,16 @@ 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.10.0
|
|
16
|
+
|
|
17
|
+
- Background bash sessions support launch, polling, input and stop with bounded
|
|
18
|
+
transcripts, deadlines, ownership and process-tree cleanup. macOS/Linux offer
|
|
19
|
+
interactive PTYs; Windows uses pipes and explicitly rejects PTYs.
|
|
20
|
+
- Literal argv stays native across platforms. Session changes, file commits,
|
|
21
|
+
read provenance and error diagnostics retain their safety boundaries during
|
|
22
|
+
asynchronous work. See [verification](#verification) for the cross-platform
|
|
23
|
+
Node/Bun matrix and its limits.
|
|
24
|
+
|
|
15
25
|
## What is new in 0.9.0
|
|
16
26
|
|
|
17
27
|
- **Text clipping does not stop batches:** sequential and parallel programs keep
|
|
@@ -49,9 +59,9 @@ transactional file operations, batching, bounded results and the grouped nova UI
|
|
|
49
59
|
tool ownership, source ranking and rendering have separate modules. All read
|
|
50
60
|
modes, batching, checkpoints and rollback behavior remain supported.
|
|
51
61
|
|
|
52
|
-
**
|
|
53
|
-
actual Pi/OMP
|
|
54
|
-
[verification results and coverage limits](#verification) below.
|
|
62
|
+
**0.9.0 release-candidate baseline:** 315/315 package tests passed on Node and
|
|
63
|
+
Bun, plus actual Pi/OMP and clean tarball installation checks. See the newer
|
|
64
|
+
[cross-platform verification results and coverage limits](#verification) below.
|
|
55
65
|
|
|
56
66
|
### Concurrent file operations
|
|
57
67
|
|
|
@@ -230,10 +240,14 @@ settings. The runtime does not silently rewrite your tool policy.
|
|
|
230
240
|
| `bash` | `bash({command,args:[...]})`; literal executable argv, without shell expansion of argument strings |
|
|
231
241
|
|
|
232
242
|
`bash` also accepts `timeout` in seconds for familiar object arguments. `timeoutMs`
|
|
233
|
-
is milliseconds and takes precedence. The owned
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
is milliseconds and takes precedence. The owned adapter launches executable argv
|
|
244
|
+
directly on every supported OS, including Windows; use the string form for shell
|
|
245
|
+
builtins, functions or startup hooks. Literal argv never enters a captured shell
|
|
246
|
+
executor. Windows string commands require native Git Bash on `PATH`, not WSL's
|
|
247
|
+
`System32/bash.exe`; source search requires a spawnable native `rg.exe`. Put their
|
|
248
|
+
real executable directories before WSL or broken WinGet links in the host's `PATH`.
|
|
249
|
+
Windows limits process command lines to 32K characters; use a workspace script file
|
|
250
|
+
for larger payloads. Argument payloads are not repeated in owned direct-execution errors;
|
|
237
251
|
stdout/stderr, exit status and source context remain. Session environment variables are taken
|
|
238
252
|
from the current execution context, not inherited from a different parent session.
|
|
239
253
|
|
|
@@ -503,10 +517,11 @@ per-program rollback. Missing files are created. Multiple invocations are not
|
|
|
503
517
|
one atomic transaction: for an all-or-nothing publication, assemble a new staging
|
|
504
518
|
file and publish it only when complete. External write overrides reject append.
|
|
505
519
|
|
|
506
|
-
Supernova
|
|
507
|
-
|
|
508
|
-
progress records under `.work`.
|
|
509
|
-
|
|
520
|
+
Supernova supports bounded foreground execution and session-owned background terminals
|
|
521
|
+
(see below), not durable jobs across host restarts. For long archive scans, use a
|
|
522
|
+
background terminal or resumable chunks and write progress records under `.work`.
|
|
523
|
+
Foreground shell commands inherit the current program `timeoutMs` unless they
|
|
524
|
+
specify their own; increasing the outer deadline no longer
|
|
510
525
|
leaves a hidden 60-second shell cap. Set the inner `bash` timeout shorter than the
|
|
511
526
|
outer program timeout (for example 10 seconds inside a 20-second program). The outer
|
|
512
527
|
deadline covers **all** waits and commands, including `sleep`; a shell's own `timeout`
|
|
@@ -515,10 +530,106 @@ pending host calls get a bounded 250ms drain to retain owned-shell diagnostics a
|
|
|
515
530
|
finalize process termination. Non-cooperating host executors may still outlive that
|
|
516
531
|
drain. Cancellation is reported separately from timeout; neither triggers a retry.
|
|
517
532
|
Progress files survive shell execution but staged VFS writes may roll back.
|
|
533
|
+
On macOS/Linux, foreground and background commands share process-group cleanup:
|
|
534
|
+
normal leader exit, cancellation and timeout retire ordinary descendants before
|
|
535
|
+
reporting completion. A shell's `command &` does not create a durable job; launch
|
|
536
|
+
the long-running command itself with `background:true` instead.
|
|
518
537
|
|
|
519
538
|
Large returned objects are bounded previews, not retained artifacts. Select fields
|
|
520
539
|
and array windows before returning, rather than parsing a truncated preview.
|
|
521
540
|
|
|
541
|
+
## Background terminal sessions
|
|
542
|
+
|
|
543
|
+
Start a command without waiting for it to finish:
|
|
544
|
+
|
|
545
|
+
```js
|
|
546
|
+
return await bash({command:"npm test", background:true});
|
|
547
|
+
// {sessionId, pid, status:"running", pty:false, output, outputStart, cursor,
|
|
548
|
+
// truncated:false, exitCode:null, signal:null}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Use `pty:true` for interactive programs that require a terminal, such as a
|
|
552
|
+
browser/passkey publisher. Literal argv remains supported:
|
|
553
|
+
|
|
554
|
+
```js
|
|
555
|
+
return await bash({command:"npm", args:["publish"], background:true, pty:true});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Starting a publish still requires the user's authorization. This API does not
|
|
559
|
+
approve commands, bypass shell overrides or provide credentials. PTY mode uses
|
|
560
|
+
`/usr/bin/script` on macOS/Linux and fails explicitly if unavailable; it adds no
|
|
561
|
+
npm/native dependency. Pipes are the default. PTY output includes terminal echo,
|
|
562
|
+
ANSI escapes and CRLF; it is a transcript, not a full-screen terminal renderer.
|
|
563
|
+
PTY commands start at 80 columns by 24 rows; dynamic resizing is not supported.
|
|
564
|
+
|
|
565
|
+
Control the returned ID in a later Supernova invocation:
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
return await bash({sessionId:data.sessionId, action:"poll", cursor:0, waitMs:1000});
|
|
569
|
+
// waitMs waits for new output/exit only when cursor is at the current end.
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
```js
|
|
573
|
+
await bash({sessionId:data.sessionId, action:"write", input:"yes\n"});
|
|
574
|
+
return await bash({sessionId:data.sessionId, action:"poll", cursor:data.cursor});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
return await bash({action:"list"});
|
|
579
|
+
// Or: return await bash({sessionId:data.sessionId, action:"stop"});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
- Results are objects, not encoded JSON or foreground text. `poll` returns
|
|
583
|
+
`running`, `exited`, `stopped`, `timed_out`, or `failed`, with nullable `exitCode`
|
|
584
|
+
and `signal`. A nonzero child exit does not throw from `poll`; inspect it.
|
|
585
|
+
Invalid arguments/IDs, startup failures and failed controls do throw. In PTY
|
|
586
|
+
mode, `exitCode` is the system `script` utility's status; signal termination
|
|
587
|
+
may be encoded there instead of in `signal` and is platform-dependent.
|
|
588
|
+
- `cursor` is an absolute UTF-16 output offset. Pass the previous cursor for
|
|
589
|
+
incremental output, or omit it to replay retained output. stdout/stderr share
|
|
590
|
+
a 65,536-character tail. `truncated:true` and `outputStart` disclose discarded
|
|
591
|
+
history; polling does not consume it. `waitMs` is 0--30,000 (default 0).
|
|
592
|
+
- Input is literal, at most 16,384 characters per call. Include `\n` for Enter
|
|
593
|
+
and `\u0003` for Ctrl-C in a PTY. Input is not echoed into tool traces, but
|
|
594
|
+
the child/terminal may echo it into output. Do not send secrets casually.
|
|
595
|
+
- Up to 8 running jobs and 32 retained sessions per extension instance. Oldest
|
|
596
|
+
completed sessions are evicted when needed. `list` returns metadata without
|
|
597
|
+
replaying output. IDs are scoped to the originating Pi session and workspace;
|
|
598
|
+
controls still waiting when that session closes reject rather than returning
|
|
599
|
+
results from the closed session.
|
|
600
|
+
- A job defaults to a **30-minute deadline**, independently of the starting
|
|
601
|
+
Supernova call. Set its `timeoutMs` explicitly to change that deadline.
|
|
602
|
+
Outer program timeouts/cancellation still bound start and control calls;
|
|
603
|
+
cancelling a poll does not stop its job. Use `stop` to terminate it.
|
|
604
|
+
- Launch, input and stop are external-mutation barriers; staged edits flush
|
|
605
|
+
first. List/poll do not flush staged edits. None runs inside an edit checkpoint.
|
|
606
|
+
Background effects are not transactional and may race edits; ordinary CAS
|
|
607
|
+
checks still detect changed file bytes, not all external side effects.
|
|
608
|
+
- On macOS/Linux, normal exit, explicit stop, job deadline and session shutdown
|
|
609
|
+
clean up owned process groups, escalating to kill when necessary. Completion
|
|
610
|
+
is reported only after cleanup, so ordinary orphaned children are not left
|
|
611
|
+
running. Failed cleanup remains listed and consumes a job slot until a later
|
|
612
|
+
stop/shutdown succeeds; it is not silently discarded. Windows pipe cleanup is
|
|
613
|
+
best-effort (`taskkill` while the leader is alive), not POSIX group supervision.
|
|
614
|
+
Jobs do not survive reload, session
|
|
615
|
+
replacement, host restart or crash as managed sessions. Deliberately detached
|
|
616
|
+
daemons are not a supported supervision model. Cleanup errors are reported.
|
|
617
|
+
Launches and controls from an old session generation are rejected, including
|
|
618
|
+
after a reload that retains the same Pi session ID. A guest started before
|
|
619
|
+
replacement cannot list, poll, write to or stop the replacement's jobs.
|
|
620
|
+
Identity is captured once per tool invocation, including queued sequential and
|
|
621
|
+
parallel batch entries, rather than reread from a mutable host SessionManager.
|
|
622
|
+
Replacement invalidates the whole guest bridge: further commands, delegated
|
|
623
|
+
calls, session-resource lookups and pending commits are rejected. Queued commits
|
|
624
|
+
recheck identity after waiting, and staged replacements recheck before each
|
|
625
|
+
installation; a failed transaction restores any earlier replacements. Delegated
|
|
626
|
+
executors recheck immediately before dispatch. External effects that already
|
|
627
|
+
started or committed are not rolled back by a session change.
|
|
628
|
+
- Background actions require Supernova's owned shell adapter. A registered shell
|
|
629
|
+
override is rejected rather than silently ignoring background options or
|
|
630
|
+
bypassing the override's permissions.
|
|
631
|
+
|
|
632
|
+
|
|
522
633
|
## JSON reports and targeted text audits
|
|
523
634
|
|
|
524
635
|
For JSON, select fields inside the read adapter, **before** output budgeting:
|
|
@@ -547,6 +658,15 @@ Plain .json reads are raw text, including malformed JSON; parsing is requested
|
|
|
547
658
|
only by `json`. Explicit line windows are not necessarily JSON documents.
|
|
548
659
|
Do not combine json with complete, line windows, or source views. External read
|
|
549
660
|
overrides reject JSON projection rather than silently ignoring the option.
|
|
661
|
+
Explicit `read({path:"TOKEN"})` / `read({target:"TOKEN"})` and path arrays mean
|
|
662
|
+
filesystem reads even for extensionless names. Missing files throw, including
|
|
663
|
+
with `resolve:false`, `complete:true` or line windows. Only a bare string without
|
|
664
|
+
those options guesses between a filename and a symbol; use `{query:"symbol"}`
|
|
665
|
+
or `resolve:true` when source search is intended.
|
|
666
|
+
Source reads protect the resolved file from accidental `write` replacement even
|
|
667
|
+
when the result is text or arrives in a streamed batch. JSON document fields such
|
|
668
|
+
as `path` and `status` are data, never evidence that another file was read.
|
|
669
|
+
|
|
550
670
|
Uncaught read errors abort the program, including `return {a:await read(...),
|
|
551
671
|
b:await read(...)}`; earlier successful values are not an implicit partial return.
|
|
552
672
|
For optional sources, explicitly return `await Promise.allSettled(paths.map(path =>
|
|
@@ -609,8 +729,15 @@ are outside the VFS counters; this is not a filesystem audit.
|
|
|
609
729
|
`{ok:true,committed:true,value}` on success. On failure it rolls back and rethrows
|
|
610
730
|
the cause, so an ignored failed checkpoint cannot report program success. Use
|
|
611
731
|
`try { await edit(async () => {...}); } catch (error) {...}` for deliberate recovery.
|
|
612
|
-
Shell commands, overlapping/nested
|
|
613
|
-
the active callback are rejected.
|
|
732
|
+
Shell commands (including background terminal controls), overlapping/nested
|
|
733
|
+
checkpoints, and concurrent commands outside the active callback are rejected.
|
|
734
|
+
Await the checkpoint before running shell checks. A checkpoint cannot roll back
|
|
735
|
+
external shell effects. For a temporary mutation test, save the original text,
|
|
736
|
+
edit and run the check outside a checkpoint, then explicitly restore it. Catch
|
|
737
|
+
the check failure, restore, and let that program succeed so the restoration
|
|
738
|
+
commits before reporting the failure. A `finally` restoration followed by an
|
|
739
|
+
uncaught error is only staged and rolls back too. Keep a backup: cancellation
|
|
740
|
+
or a worker deadline can prevent cleanup from running.
|
|
614
741
|
|
|
615
742
|
## Context, caching and failure fidelity
|
|
616
743
|
|
|
@@ -718,6 +845,7 @@ delivery, and execution-context environment.
|
|
|
718
845
|
| Read routing and typed readers | `adapters/read.js`, `adapters/read-{text,json,image,focus}.js` |
|
|
719
846
|
| Image validation and isolated decoding | `shared/png.js`, `shared/image.js`, `shared/image-worker.js` |
|
|
720
847
|
| Bounded I/O and atomic transactions | `fs/file-io.js`, `fs/read-window.js`, `fs/vfs.js`, `fs/commit.js` |
|
|
848
|
+
| Foreground/background process ownership | `fs/workspace.js`, `fs/process-tree.js`, `fs/background.js` |
|
|
721
849
|
| Source search and ranking | `context/query.js`, `context/snap-search.js`, `context/source-entry.js`, `context/evidence-{graph,rank}.js` |
|
|
722
850
|
| Model output and host rendering | `output/final.js`, `output/outcome.js`, `ui/host-render.js`, `ui/trace.js`, `ui/render.js` |
|
|
723
851
|
|
|
@@ -726,7 +854,56 @@ acyclic import graph and prohibit context modules from importing runtime code.
|
|
|
726
854
|
|
|
727
855
|
### Verification
|
|
728
856
|
|
|
729
|
-
|
|
857
|
+
**Portability pass, 2026-09-22:** the current product sources, including the
|
|
858
|
+
in-flight terminal-poll shutdown guard, passed the package suite on real hosts:
|
|
859
|
+
|
|
860
|
+
| Host | Node | Bun 1.4.0 |
|
|
861
|
+
|---|---|---|
|
|
862
|
+
| macOS, Node 26.7.0 | 357 passed | 355 passed |
|
|
863
|
+
| DGX Spark, Linux arm64, Node 24.16.0 | 357 passed | 355 passed |
|
|
864
|
+
| Windows x64, Node 24.16.0 | 348 passed, 9 skipped | 346 passed, 9 skipped |
|
|
865
|
+
|
|
866
|
+
A fresh Pi 0.87 loader followed the configured Supernova symlink into this
|
|
867
|
+
checkout, registered its single `supernova` tool, and executed read/write.
|
|
868
|
+
The restarted active extension also completed a pipe job and interactive PTY
|
|
869
|
+
with stdin, output, and exit code 7. This does not inspect Pi's in-memory
|
|
870
|
+
module cache or test remote provider sessions.
|
|
871
|
+
|
|
872
|
+
Commands: `npm test --prefix packages/pi-supernova` locally; in the standalone
|
|
873
|
+
remote package, `node --test tests/*/*.test.mjs`; for Bun, `bun --run test`
|
|
874
|
+
on a clean tree. The remote archive contained macOS AppleDouble
|
|
875
|
+
`._background.test.mjs` metadata, which Bun mistakenly discovered as a test;
|
|
876
|
+
the successful Spark/Windows Bun reruns explicitly selected real files with
|
|
877
|
+
`bun test tests/*/[!.]*.test.mjs` (PowerShell constructed the same filtered
|
|
878
|
+
file list on Windows). New macOS verification archives should use
|
|
879
|
+
`tar --no-mac-metadata --no-xattrs` to prevent AppleDouble entries. Final
|
|
880
|
+
fixture-only changes were additionally checked on macOS/Spark with
|
|
881
|
+
`node --test tests/codemode/{papercuts,guest-contracts}.test.mjs` (59 passed)
|
|
882
|
+
and `bun test tests/codemode/{papercuts,guest-contracts}.test.mjs` (57 passed);
|
|
883
|
+
Windows reran the full suite. Totals are reported by each runtime's test runner.
|
|
884
|
+
|
|
885
|
+
Windows verification selected native Git Bash and the real ripgrep executable
|
|
886
|
+
directory in the test process's `PATH`. Its SSH token's backup/restore privileges
|
|
887
|
+
were disabled only for the test process so real deny-read/execute/write ACLs
|
|
888
|
+
could exercise permission errors. No machine-wide configuration was changed.
|
|
889
|
+
Bun 1.4.0 was installed into the temporary Windows verification directory;
|
|
890
|
+
the machine's older Bun 1.3.14 test runner is not a passing result.
|
|
891
|
+
|
|
892
|
+
The nine Windows skips remain explicit POSIX/platform-specific cases: shell
|
|
893
|
+
startup/quoted-script behavior, FIFO admission, alias and descendant semantics,
|
|
894
|
+
and the large JSON allocation case. Windows literal argv now runs the same
|
|
895
|
+
ownership/malformed-argument tests as POSIX. PTY tests verify explicit Windows
|
|
896
|
+
rejection rather than silently skipping or treating pipes as terminals.
|
|
897
|
+
|
|
898
|
+
Mutation checks rejected lost native-argv ownership, missing-parent and source/
|
|
899
|
+
evidence-path regressions, dropped cleanup errors, and unmapped launch failures.
|
|
900
|
+
A surviving ambiguous-source-path mutant led to a stronger regression. Tests also
|
|
901
|
+
exercise both synchronous Windows and asynchronous POSIX launch failures.
|
|
902
|
+
These results do not claim native Windows PTYs, exhaustive interleavings, or
|
|
903
|
+
new Pi/OMP/provider end-to-end coverage on the remote hosts. Root release gates
|
|
904
|
+
were not run in this pass.
|
|
905
|
+
|
|
906
|
+
The historical 0.9.0 release candidate was verified on **macOS**:
|
|
730
907
|
|
|
731
908
|
| Check | Result |
|
|
732
909
|
|---|---|
|
package/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {programParameters} from './src/contract/program.js';
|
|
2
2
|
import {progressEmitter} from './src/ui/progress.js';
|
|
3
|
+
|
|
3
4
|
export {progressEmitter} from './src/ui/progress.js';
|
|
5
|
+
|
|
4
6
|
import {result,errorText,successText,fitOutput,attachReceipts,throwIfFailed} from './src/output/outcome.js';
|
|
5
7
|
|
|
6
8
|
import { runProgramBatch } from "./src/runtime/program-batch.js";
|
|
@@ -77,6 +79,7 @@ export function registerCodeMode(pi) {
|
|
|
77
79
|
|
|
78
80
|
function rejectLoneParallel(params) {
|
|
79
81
|
if (params?.parallel !== undefined) throw new Error("parallel applies to the programs array; no commands ran");
|
|
82
|
+
|
|
80
83
|
if (params?.mergeData !== undefined) throw new Error("mergeData applies to the programs array; no commands ran");
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -90,8 +93,8 @@ export function registerCodeMode(pi) {
|
|
|
90
93
|
return { runController, abortRun };
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
function openRunBridge(ctx, runCwd, budget, runController, timeoutMs) {
|
|
94
|
-
const runBridge = bridge.fork({ getCwd: () => runCwd, budget, timeoutMs });
|
|
96
|
+
function openRunBridge(ctx, runCwd, budget, runController, timeoutMs, terminalIdentity) {
|
|
97
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd, budget, timeoutMs, terminalIdentity });
|
|
95
98
|
runBridge.bindCallContext(ctx, runController.signal);
|
|
96
99
|
runBridge.resetCallBudget();
|
|
97
100
|
|
|
@@ -101,6 +104,7 @@ export function registerCodeMode(pi) {
|
|
|
101
104
|
async function runAndCommit(params, runCwd, runBridge, abortRun, runController, budget) {
|
|
102
105
|
refreshCatalog(runBridge);
|
|
103
106
|
runBridge.beginSpeculation();
|
|
107
|
+
|
|
104
108
|
const outcome = await runGuestProgram({
|
|
105
109
|
code: params?.code,
|
|
106
110
|
file: params?.file,
|
|
@@ -111,6 +115,7 @@ export function registerCodeMode(pi) {
|
|
|
111
115
|
signal: runController.signal,
|
|
112
116
|
onTimeout: abortRun,
|
|
113
117
|
});
|
|
118
|
+
|
|
114
119
|
runBridge.close();
|
|
115
120
|
|
|
116
121
|
if (outcome.ok) {
|
|
@@ -153,6 +158,7 @@ export function registerCodeMode(pi) {
|
|
|
153
158
|
attachReceipts(outcome, trace);
|
|
154
159
|
const bounded = fitOutput(outcome, call, config.maxReturnChars, outcome.ok ? successText : errorText);
|
|
155
160
|
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
161
|
+
|
|
156
162
|
const response = result(visible, {
|
|
157
163
|
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
158
164
|
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
@@ -177,12 +183,22 @@ export function registerCodeMode(pi) {
|
|
|
177
183
|
renderCall: renderSupernovaCall,
|
|
178
184
|
renderResult: renderSupernovaResult,
|
|
179
185
|
execute: async function execute(_id, params, signal, onUpdate, ctx, budget, runOpts) {
|
|
180
|
-
|
|
186
|
+
const runCwd = isString(ctx?.cwd) && ctx.cwd ? ctx.cwd : cwd;
|
|
187
|
+
// The entire invocation owns one identity, including entries queued by a
|
|
188
|
+
// sequential/parallel batch. Dispatch after reload cannot renew its lease.
|
|
189
|
+
const terminalIdentity = runOpts?.terminalIdentity ?? bridge.captureTerminalIdentity(ctx, runCwd);
|
|
190
|
+
|
|
191
|
+
if (params?.programs !== undefined) {
|
|
192
|
+
const entry = (id, payload, abort, update, context, sharedBudget, options) =>
|
|
193
|
+
execute(id, payload, abort, update, context, sharedBudget, {...options,terminalIdentity});
|
|
194
|
+
|
|
195
|
+
return runProgramBatch(_id,params,signal,onUpdate,ctx,config,entry);
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
rejectLoneParallel(params);
|
|
182
199
|
cancelWarmTimer();
|
|
183
|
-
const runCwd = isString(ctx?.cwd) && ctx.cwd ? ctx.cwd : cwd;
|
|
184
200
|
const { runController, abortRun } = bindRunSignal(signal);
|
|
185
|
-
const runBridge = openRunBridge(ctx, runCwd, budget, runController, params?.timeoutMs);
|
|
201
|
+
const runBridge = openRunBridge(ctx, runCwd, budget, runController, params?.timeoutMs, terminalIdentity);
|
|
186
202
|
const call = ++programSeq;
|
|
187
203
|
runBridge.ledger.beginProgram(call);
|
|
188
204
|
const emitProgress = progressEmitter(onUpdate);
|
|
@@ -205,6 +221,7 @@ export function registerCodeMode(pi) {
|
|
|
205
221
|
} finally {
|
|
206
222
|
peakSeen = Math.max(peakSeen, overlapPeak);
|
|
207
223
|
inFlight -= 1;
|
|
224
|
+
|
|
208
225
|
if (inFlight === 0) overlapPeak = 0;
|
|
209
226
|
finishRun(runBridge, emitProgress, signal, abortRun, runController);
|
|
210
227
|
}
|
|
@@ -222,10 +239,14 @@ export function registerCodeMode(pi) {
|
|
|
222
239
|
});
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
pi.on("session_shutdown", () => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
242
|
+
pi.on("session_shutdown", async () => {
|
|
243
|
+
stopped = true;
|
|
244
|
+
cancelWarmTimer();
|
|
245
|
+
await Promise.all([stopWarmGuestWorker(), bridge.shutdownTerminals()]);
|
|
246
|
+
});
|
|
247
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
248
|
+
await bridge.shutdownTerminals();
|
|
249
|
+
bridge.reopenTerminals();
|
|
229
250
|
stopped = false;
|
|
230
251
|
|
|
231
252
|
cwd = ctx && isString(ctx.cwd) && ctx.cwd ? ctx.cwd : process.cwd();
|
package/package.json
CHANGED
package/src/adapters/bash.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import { readResult } from "../shared/result.js";
|
|
2
3
|
|
|
3
4
|
import { normalizeBash } from "../contract/bash.js";
|
|
4
5
|
import { sourceForReferences } from "../fs/source-window.js";
|
|
@@ -6,12 +7,15 @@ import { resolveWorkspacePath, runCommand, clearPathCache } from "../fs/workspac
|
|
|
6
7
|
|
|
7
8
|
export function createBash(ctx) {
|
|
8
9
|
const { getCwd, vfs, config, index, ledger, hooks } = ctx;
|
|
10
|
+
|
|
9
11
|
function combineBashText(stdout, stderr) {
|
|
10
12
|
return stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
async function bash(params, signal) {
|
|
14
16
|
params = normalizeBash(params);
|
|
17
|
+
|
|
18
|
+
if (params.background === true || params.action !== undefined) return background(params, signal);
|
|
15
19
|
const cwd = getCwd();
|
|
16
20
|
const command = String(params.command);
|
|
17
21
|
const literal = Array.isArray(params.args);
|
|
@@ -47,6 +51,7 @@ export function createBash(ctx) {
|
|
|
47
51
|
if (!literal && /\bbash: (?:-c: )?line \d+: (?:syntax error|unexpected EOF)/.test(stderr)) {
|
|
48
52
|
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.';
|
|
49
53
|
}
|
|
54
|
+
|
|
50
55
|
text += await sourceForReferences(cwd, targetCwd, text, signal, ledger);
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -57,14 +62,49 @@ export function createBash(ctx) {
|
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
async function background(params, signal) {
|
|
66
|
+
const manager = hooks.terminals;
|
|
67
|
+
const owner = hooks.terminalOwner();
|
|
68
|
+
vfs.assertExternalAllowed("bash");
|
|
69
|
+
const mutating = params.background === true || params.action === "write" || params.action === "stop";
|
|
70
|
+
let targetCwd;
|
|
71
|
+
|
|
72
|
+
if (params.background === true) {
|
|
73
|
+
await manager.validateStart(params, hooks.terminalGeneration);
|
|
74
|
+
targetCwd = await commandCwd(params, getCwd());
|
|
75
|
+
} else manager.validateControl(params, owner, hooks.terminalGeneration);
|
|
76
|
+
const transactionBarrier = mutating ? await vfs.prepareExternalMutation("bash") : false;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const value = params.background === true
|
|
80
|
+
? await manager.start(Array.isArray(params.args) ? [params.command,...params.args] : ["bash","-c",params.command], {
|
|
81
|
+
...params, cwd:targetCwd, owner, env:hooks.commandEnv(), signal, generation:hooks.terminalGeneration,
|
|
82
|
+
// Asynchronous completion must not erase an active program's CAS observations.
|
|
83
|
+
changed:()=>{index.invalidate(); hooks.workspaceChanged();},
|
|
84
|
+
})
|
|
85
|
+
: await manager.control(params, owner, signal, hooks.terminalGeneration);
|
|
86
|
+
|
|
87
|
+
// A completed job's nonzero exit is status data, not a failure to poll it.
|
|
88
|
+
return readResult(value, {background:true,transactionBarrier}, Array.isArray(value) ? `${value.length} background terminals` : `terminal ${value.sessionId}: ${value.status}`);
|
|
89
|
+
} finally {
|
|
90
|
+
if (mutating) { vfs.invalidateObserved(); hooks.workspaceChanged(); }
|
|
91
|
+
|
|
92
|
+
index.invalidate();
|
|
93
|
+
clearPathCache();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
return { bash };
|
|
61
98
|
}
|
|
62
99
|
|
|
63
100
|
async function commandCwd(params, cwd) {
|
|
64
101
|
const target = params.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
102
|
+
|
|
65
103
|
if (params.cwd !== undefined) {
|
|
66
104
|
const stat = await fs.stat(target).catch(() => null);
|
|
105
|
+
|
|
67
106
|
if (!stat?.isDirectory()) throw new Error("bash cwd is not a directory: " + params.cwd);
|
|
68
107
|
}
|
|
108
|
+
|
|
69
109
|
return target;
|
|
70
110
|
}
|
package/src/adapters/read.js
CHANGED
|
@@ -24,6 +24,7 @@ export function createRead(ctx) {
|
|
|
24
24
|
const readTextFile = createTextReader(ctx, readWindow);
|
|
25
25
|
const maybeImage = createImageReader(vfs);
|
|
26
26
|
const focusAbout = createFocusedReader(vfs, readBudget);
|
|
27
|
+
|
|
27
28
|
async function sourceRead(query, searchDir, signal, params = {}) {
|
|
28
29
|
params = { ...params, resolve: params.resolve !== false };
|
|
29
30
|
const cwd = getCwd();
|
|
@@ -43,6 +44,7 @@ export function createRead(ctx) {
|
|
|
43
44
|
const overlay = vfs.getOverlay(target);
|
|
44
45
|
|
|
45
46
|
if (overlay !== undefined) return Buffer.byteLength(overlay, "utf8") > 512 * 1024;
|
|
47
|
+
|
|
46
48
|
try { return (await fs.stat(target)).size > 512 * 1024; } catch { return false; }
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -57,6 +59,7 @@ export function createRead(ctx) {
|
|
|
57
59
|
const opened = await readFile(target, bounded
|
|
58
60
|
? { ...params, about: undefined, offset: Math.max(1, result.line - 4), limit: params.limit ?? 120 }
|
|
59
61
|
: { ...params, about: undefined }, result.line, result.path, bounded ? undefined : query, signal);
|
|
62
|
+
|
|
60
63
|
const block = opened.content?.[0];
|
|
61
64
|
|
|
62
65
|
if (block?.type !== "text") throw new Error("source resolution requires a text file; read the image path directly");
|
|
@@ -75,14 +78,16 @@ export function createRead(ctx) {
|
|
|
75
78
|
function foundSource(result, params, block, details, firstLine, lastLine, sourceChars, nextOffset, complete) {
|
|
76
79
|
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
77
80
|
text: block.text.slice(0, sourceChars), complete };
|
|
81
|
+
|
|
78
82
|
if (nextOffset !== undefined) source.nextOffset = nextOffset;
|
|
79
83
|
|
|
80
84
|
return readResult(params.resolve ? source : "// " + result.path + ":" + firstLine + "-" + lastLine + "\n" + block.text,
|
|
81
|
-
{ ...details, isSnap: true });
|
|
85
|
+
{ ...details, isSnap: true, sourcePath:result.path });
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
async function addBatchItem(state, index, raw, onItem) {
|
|
85
89
|
const bytes = raw[READ_BYTES];
|
|
90
|
+
|
|
86
91
|
if (!onItem) {
|
|
87
92
|
if (state.bytes + bytes > MAX_READ_VALUE_BYTES) throw new Error("batched read exceeds " + MAX_READ_VALUE_BYTES + " bytes; use individual reads");
|
|
88
93
|
} else if (!state.streamed && state.bytes + bytes > 65536) {
|
|
@@ -90,12 +95,14 @@ export function createRead(ctx) {
|
|
|
90
95
|
const retained = state.items.splice(0);
|
|
91
96
|
await Promise.all(retained.map(async (item,i) => { if (item) await onItem(i,item); }));
|
|
92
97
|
}
|
|
98
|
+
|
|
93
99
|
if (state.streamed) await onItem(index,raw);
|
|
94
100
|
else { state.items[index] = raw; state.bytes += bytes; }
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
async function readBatchItem(params, target, index, signal, state, onItem) {
|
|
98
104
|
let raw;
|
|
105
|
+
|
|
99
106
|
try { raw = asReadResult(await readSingle({...params,path:target,target:undefined},getCwd(),target,signal)); }
|
|
100
107
|
catch (error) {
|
|
101
108
|
signal?.throwIfAborted();
|
|
@@ -103,30 +110,41 @@ export function createRead(ctx) {
|
|
|
103
110
|
raw = readResult(error.message,{path:target});
|
|
104
111
|
raw.isError = true;
|
|
105
112
|
}
|
|
113
|
+
|
|
114
|
+
state.sourcePaths[index] = raw.details?.sourcePath;
|
|
106
115
|
await addBatchItem(state,index,raw,onItem);
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
async function readBatch(params, signal, onItem) {
|
|
110
|
-
const state = {items:[],errors:Array(params.path.length).fill(null),bytes:0,streamed:false};
|
|
119
|
+
const state = {items:[],sourcePaths:[],errors:Array(params.path.length).fill(null),bytes:0,streamed:false};
|
|
120
|
+
|
|
111
121
|
// Delivery/acknowledgement stays inside the same eight-operation scheduler
|
|
112
122
|
// slot as I/O. A busy guest cannot cause unbounded host/message-queue buffering.
|
|
113
123
|
const settled = await Promise.allSettled(params.path.map((target,index) =>
|
|
114
124
|
reads.schedule("read",()=>readBatchItem(params,target,index,signal,state,onItem),signal)));
|
|
125
|
+
|
|
115
126
|
const failed = settled.find(result=>result.status === "rejected");
|
|
127
|
+
|
|
116
128
|
if (failed) throw failed.reason;
|
|
117
129
|
signal?.throwIfAborted();
|
|
130
|
+
|
|
118
131
|
const response = readResult("",{count:params.path.length,batch:true,independent:params._independent===true,
|
|
119
132
|
jsonMany:Array.isArray(params.json),streamed:state.streamed,
|
|
120
|
-
items:state.streamed ? [] : state.items.map(raw=>raw[READ_VALUE]),
|
|
133
|
+
items:state.streamed ? [] : state.items.map(raw=>raw[READ_VALUE]),
|
|
134
|
+
sourcePaths:!state.streamed && state.sourcePaths.some(isString) ? state.sourcePaths : undefined,itemErrors:state.errors,
|
|
121
135
|
errors:state.errors.flatMap((message,i)=>message ? [{path:params.path[i],message}] : [])});
|
|
136
|
+
|
|
122
137
|
response.isError = params._independent!==true && state.errors.some(Boolean);
|
|
138
|
+
|
|
123
139
|
return response;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
async function readAdapter(params, signal, onItem) {
|
|
127
143
|
signal?.throwIfAborted();
|
|
128
144
|
params = normalizeRead(normalizeReadWindow(params));
|
|
145
|
+
|
|
129
146
|
if (Array.isArray(params.path)) return readBatch(params,signal,onItem);
|
|
147
|
+
|
|
130
148
|
return reads.schedule("read",async()=>asReadResult(await readSingle(params,getCwd(),params.path,signal)),signal);
|
|
131
149
|
}
|
|
132
150
|
|
|
@@ -136,6 +154,7 @@ export function createRead(ctx) {
|
|
|
136
154
|
const cls = classifyRead(params, existing);
|
|
137
155
|
const relOf = hit => relativeSlash(cwd, hit.path);
|
|
138
156
|
const snapScope = (scoped, hit) => hit?.directory ? hit.path : scoped ? resolveReadPath(cwd, params.path) : cwd;
|
|
157
|
+
|
|
139
158
|
const kinds = {
|
|
140
159
|
session: async () => {
|
|
141
160
|
const target = await resolveSessionResource(params.path, signal, hooks);
|
|
@@ -153,6 +172,7 @@ export function createRead(ctx) {
|
|
|
153
172
|
dir: () => readDirectory(cls.existing.path, signal),
|
|
154
173
|
missing: () => readResult({ status: "not_found", path: null, line: null, signature: "", confidence: 0, context: [] }, { isSnap: true }),
|
|
155
174
|
};
|
|
175
|
+
|
|
156
176
|
const run = kinds[cls.kind];
|
|
157
177
|
|
|
158
178
|
if (!run) throw new Error("unhandled read kind: " + cls.kind);
|
|
@@ -224,6 +244,7 @@ export function createRead(ctx) {
|
|
|
224
244
|
const cwd = getCwd();
|
|
225
245
|
|
|
226
246
|
if (!isString(params?.query) || !params.query.trim()) throw new Error("evidence requires query");
|
|
247
|
+
|
|
227
248
|
if (tokenizeQuery(params.query).tokens.length > 16) throw new Error("evidence query is too broad; use at most 16 keywords");
|
|
228
249
|
|
|
229
250
|
if (signal?.aborted) throw new Error("aborted");
|