@code-yeongyu/senpi-codemode 2026.10.10 → 2026.10.11
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/CHANGELOG.md +196 -0
- package/README.md +113 -11
- package/package.json +6 -5
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +9 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +52 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/editable-check.ts +1 -1
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +42 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +20 -6
- package/src/index.ts +2 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/globals.jl +128 -0
- package/src/kernels/jl/prelude.jl +15 -2
- package/src/kernels/jl/runner.jl +35 -143
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +23 -24
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/inline-worker.ts +2 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +8 -0
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-tree-host.ts +4 -8
- package/src/kernels/js/process-tree.d.ts +11 -0
- package/src/kernels/js/process-tree.js +55 -10
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/windows-tree-kill-host.ts +117 -0
- package/src/kernels/js/windows-tree-kill.d.ts +19 -0
- package/src/kernels/js/windows-tree-kill.js +100 -0
- package/src/kernels/js/worker-core.js +51 -8
- package/src/kernels/js/worker-cwd.js +46 -12
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +66 -13
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +9 -0
- package/src/kernels/js/worker-slot.ts +8 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/prelude.py +69 -27
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/sandbox-cell.ts +26 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/shared/bun-ffi.ts +24 -0
- package/src/kernels/shared/kernel-memory-host.ts +30 -7
- package/src/kernels/shared/process-cpu.ts +7 -3
- package/src/kernels/shared/process-group-cpu.ts +6 -3
- package/src/kernels/shared/subprocess-kernel.ts +29 -3
- package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
- package/src/kernels/shared/subprocess-process.ts +24 -2
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +3 -0
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-tool.ts +4 -2
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render-agent.ts +110 -0
- package/src/tool/render-blocks.ts +252 -0
- package/src/tool/render-call.ts +130 -0
- package/src/tool/render-cell.ts +165 -0
- package/src/tool/render-detail.ts +152 -0
- package/src/tool/render-live.ts +288 -0
- package/src/tool/render-result.ts +202 -0
- package/src/tool/render-status.ts +166 -0
- package/src/tool/render-tail.ts +139 -0
- package/src/tool/render.ts +3 -1069
- package/src/tool/run-eval-cell.ts +24 -14
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,202 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.11] - 2026-10-11
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Julia loads handle helpers at first use and installs globals sizing declarations only when a globals diagnostic is requested, reducing fresh-kernel CPU work. This is a partial fix; the remaining startup regression is tracked separately (Refs [#3048](https://github.com/code-yeongyu/senpi/issues/3048)).
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
## [2026.10.10-12] - 2026-10-10
|
|
30
|
+
|
|
31
|
+
### Breaking Changes
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Benchmark interpreter CPU is sampled by the host after complete result reception, including result encoding and flushing. Saved reports record this boundary and refuse legacy embedded-clock CPU samples that need remeasurement (Refs [#3048](https://github.com/code-yeongyu/senpi/issues/3048)).
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
## [2026.10.10-11] - 2026-10-09
|
|
44
|
+
|
|
45
|
+
### Breaking Changes
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Replaced the plan-authored type assertions the F2 audit listed, in this package's production code and tests, with real types: `EvalKernel` now declares the optional `describeKernelTools` / `invokeKernelTool` capabilities, the JS `WorkerLike` carries an optional `pid`, `bun:ffi` is read through one shared `isBunFfi` guard, the sandbox `credits` buffer is typed as `Int32Array<SharedArrayBuffer>`, the gate allowlist is validated by the existing typebox `allowlistSchema`, and test fakes use `satisfies`, typed factories and narrowing guards instead of casts. No behavior change, except that a malformed base allowlist is now refused by the same schema the head allowlist already required. ([#3005](https://github.com/code-yeongyu/senpi/issues/3005))
|
|
52
|
+
- The bench judges `crash-queue-100` on Ruby and Julia by an absolute head budget instead of the head/base ratio: all 100 queued cells must run exactly once after one kernel replacement, with the head's median wall time under 820 ms (Ruby) or 2,700 ms (Julia). Their base never recovered from the crash, so the ratio compared a failure path with the real work. ([#3025](https://github.com/code-yeongyu/senpi/issues/3025))
|
|
53
|
+
- A streaming, queued or running eval row now shows the cell's code in a fixed-height block that scrolls upward as lines arrive: the newest line always stays visible, older lines fold into an "N earlier code lines" row counted inside the block, and once output or status events arrive they share the block's rows instead of growing it, so the row never grows the transcript at any terminal width. The header stays one row, keeping the live spinner and render-clock elapsed time (a queued cell keeps its queued badge). Once a cell finishes, errs or is cancelled, the collapsed row is one line (icon, summary, status and duration), and expanding it shows the full code, output and status events. ([#2933](https://github.com/code-yeongyu/senpi/issues/2933))
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Python cells use a C-backed FIFO and one combined stdout/stderr routing scope, reducing per-cell bookkeeping while retaining interpreter ownership, parked-cell callbacks, invocation scope, cancellation, and stale-descriptor fencing ([#3034](https://github.com/code-yeongyu/senpi/issues/3034)).
|
|
58
|
+
|
|
59
|
+
- Ruby and Julia cells below the host-read memory notice threshold and ceiling no longer walk all globals or serialize an unused memory payload. Above either threshold, successful and raised cells both collect the same bounded globals diagnostic, including when hysteresis suppresses the notice text. Stopping while a finished cell waits for that optional diagnostic now settles its completed result without globals and keeps kernel state. Current-footprint accounting and ceiling enforcement still run on every cell ([#3028](https://github.com/code-yeongyu/senpi/issues/3028)).
|
|
60
|
+
|
|
61
|
+
- Warm eval cells now recheck the session working directory synchronously instead of awaiting a filesystem thread-pool round trip. Every cell still refuses a deleted or non-directory cwd with the same error, and other filesystem errors still surface; kernel startup retains its asynchronous pre/post checks ([#3033](https://github.com/code-yeongyu/senpi/issues/3033)).
|
|
62
|
+
|
|
63
|
+
- When a Python, Ruby, Julia or process-isolated JavaScript kernel dies while a frame is being sent to it, the failed write (EPIPE) is reported as that kernel's error instead of surfacing as an unhandled error. Before, that unhandled error could end the host, or make a test run where every test passed exit 1 ([#3016](https://github.com/code-yeongyu/senpi/issues/3016)).
|
|
64
|
+
|
|
65
|
+
- On macOS and Linux, when a JavaScript eval cell ends or is interrupted, a process its child started and left behind (re-parented to init after the child exited, as with `(sleep 30 &)`) is now stopped too: every process group the kernel gave a cell child is signalled at retirement. The agent's own group is never signalled, nor a group whose leader exited and whose pid was since reused, and children the cell started with `detached: true` are left running ([#3020](https://github.com/code-yeongyu/senpi/issues/3020)).
|
|
66
|
+
|
|
67
|
+
- On Windows, ending a kernel's process tree (cell timeout, reset, shutdown) no longer uses `taskkill /T`: the tree is computed from creation times and each process is ended by pid, so an older, unrelated process holding a recycled parent pid is never killed ([#2999](https://github.com/code-yeongyu/senpi/issues/2999)).
|
|
68
|
+
|
|
69
|
+
- `tool_schema("eval:environments")` now documents the `packages.install(manager, requirements, {timeout?})` cell helper that shipped in #2877: the signature (Python spells the option `timeout=`, default 600 s), the managers per language (`pip` for Python; `bun`/`npm` for JavaScript), the receipt fields, cancellation by `stop`, and the `environment_install_timeout` error code it raises. A two-way contract test now keeps every `environment_*`/`eval_isolate_*` code the source raises documented in its owning entry. ([#3003](https://github.com/code-yeongyu/senpi/issues/3003))
|
|
70
|
+
|
|
71
|
+
- On macOS and Linux, a process started from a JavaScript eval cell with `node:child_process` or `Bun.spawn` now gets its own process group. A cell that later stops that job by its group (`kill -TERM -- -$PGID`) therefore no longer stops the agent itself. Named imports of `node:child_process`, `node:fs` and `node:path` in a cell now get the session cwd handling too. `Bun.$`, `exec`/`execFile` and the synchronous spawners (`spawnSync`, `execSync`, `execFileSync`, `Bun.spawnSync`, which keep the terminal) still share the agent's group, so a command there that signals a process group (including `pkill -g` and `killall`) prints a notice naming the agent's group. The notice is a warning only and never blocks the cell ([#2995](https://github.com/code-yeongyu/senpi/issues/2995)).
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
## [2026.10.10-10] - 2026-10-09
|
|
76
|
+
|
|
77
|
+
### Breaking Changes
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
### Removed
|
|
86
|
+
|
|
87
|
+
## [2026.10.10-9] - 2026-10-08
|
|
88
|
+
|
|
89
|
+
### Breaking Changes
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
### Removed
|
|
98
|
+
|
|
99
|
+
## [2026.10.10-8] - 2026-10-08
|
|
100
|
+
|
|
101
|
+
### Breaking Changes
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- `packages.install(manager, requirements, {timeout?})` in JavaScript and Python cells installs through the same session environment as `%pip install` / `%bun add` / `%npm add` and returns the install receipt; a stop cancels it and the timeout (600 seconds by default) fails it with `environment_install_timeout`.
|
|
106
|
+
|
|
107
|
+
- `tool_schema("eval:environments")` and `tool_schema("eval:isolation")` document the package magics (`%pip`, `%bun`/`%npm`, `%environment`, `%load`) and isolated cells on demand, with the error codes each one raises. Nothing is added to the eval prompt or its input schema.
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- The eval timing benchmark's spike retry no longer waits for a load at or above its own refusal ceiling: the settle target is capped at 70, so a run whose calm load was near 80 cannot start its retries straight into another spike and burn them all in seconds. On a host whose normal load is above 70, each retry now waits the full 15 minutes and then retries anyway; the attempt's own spike checks still decide what is kept. ([#2922](https://github.com/code-yeongyu/senpi/issues/2922))
|
|
114
|
+
|
|
115
|
+
- The eval timing benchmark no longer throws away a whole multi-block run when the host's 1-minute load goes over 80 partway through: only the block that saw the spike is discarded and re-run (up to 3 retries, each after the load has fallen back under 70). A runtime failure in the same block is never discarded with the spike. A block that spikes on every attempt is labelled, making the run inconclusive rather than refused, and the other blocks still report. Re-run blocks are listed in the JSON report's `retriedBlocks`. ([#2909](https://github.com/code-yeongyu/senpi/issues/2909))
|
|
116
|
+
|
|
117
|
+
### Removed
|
|
118
|
+
|
|
119
|
+
## [2026.10.10-6] - 2026-10-07
|
|
120
|
+
|
|
121
|
+
### Breaking Changes
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
### Changed
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
### Removed
|
|
130
|
+
|
|
131
|
+
## [2026.10.10-5] - 2026-10-07
|
|
132
|
+
|
|
133
|
+
### Breaking Changes
|
|
134
|
+
|
|
135
|
+
### Added
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
|
|
139
|
+
- A running, queued or detached eval row now leads with the cell's summary (or its first code line), then the language, state and elapsed time, on one line; the code is shown on expand. A call still streaming its arguments shows that row instead of the raw `eval code="..."` fallback. Completed rows are unchanged ([#2802](https://github.com/code-yeongyu/senpi/issues/2802)).
|
|
140
|
+
|
|
141
|
+
- A JavaScript cell may declare a name the kernel or platform already defines (`log`, `fetch`, `print`, `URL`, ...): the value persists for your later cells while the kernel and imported libraries keep the original, `delete <name>` restores it, and the cell notes the shadowing ([#2793](https://github.com/code-yeongyu/senpi/issues/2793)).
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- A live eval row whose cell has no summary skips a first code line that holds only escape or control characters and leads with the next line that has content, instead of showing only an ellipsis ([#2850](https://github.com/code-yeongyu/senpi/issues/2850)).
|
|
146
|
+
|
|
147
|
+
- The `require` in a JavaScript cell carries `require.resolve`, `require.resolve.paths` and `require.cache` like Node's own `require`, and the call and `resolve` share one lookup, so they always name the same copy: builtins natively (including Bun's, such as `bun:sqlite`), then the project, then the managed package environment ([#2832](https://github.com/code-yeongyu/senpi/issues/2832)).
|
|
148
|
+
|
|
149
|
+
- A live eval row whose cell has no summary sanitizes its first code line before measuring it, so escape and control characters in that line never reach the one-line row ([#2839](https://github.com/code-yeongyu/senpi/issues/2839)).
|
|
150
|
+
|
|
151
|
+
- A live eval row stays one line in every terminal: its headline is measured and cut in screen cells, so a summary with wide characters (Korean, Chinese, Japanese, emoji) no longer wraps a narrow terminal, and a `peek`/`stop` call still streaming in renders `eval peek` instead of `eval peek undefined` ([#2831](https://github.com/code-yeongyu/senpi/issues/2831)).
|
|
152
|
+
|
|
153
|
+
- Stopping or timing out a JavaScript cell that awaits something that never settles (a promise, a `fetch` whose server never answers, a polling loop on timers, `Bun.sleep` or `node:timers/promises`, a loop of short `Bun.spawn` children) now keeps the worker and every global instead of restarting it. Stop ends the cell and everything it started: its timers are cleared, pending sleeps and `fetch` requests reject, and the sockets, servers, WebSockets, WebViews, nested workers and child processes it opened are closed; the result arrives once its children are gone. A stopped cell's own `catch`/`finally` can no longer print, call tools, start processes, schedule timers or open connections. An unhandled promise rejection no longer crashes the JavaScript kernel: it is reported on the running or next cell, naming the cell it came from, with bursts folded into one line; an uncaught exception still restarts the worker. A cell stopped during a `Bun.$` command still restarts the worker, as before ([#2788](https://github.com/code-yeongyu/senpi/issues/2788)).
|
|
154
|
+
|
|
155
|
+
- JavaScript cells can call `require(...)` and `createRequire(...)`: builtins, relative CommonJS and JSON files, and packages from the project or the managed package environment resolve as they do for `import` ([#2792](https://github.com/code-yeongyu/senpi/issues/2792)).
|
|
156
|
+
|
|
157
|
+
- A detached eval cell's completion notification now carries the same output its result would have shown in the foreground (head, tail, elision marker and full-output notice) instead of a 512-byte tail, and images the cell displayed are delivered with the notification ([#2789](https://github.com/code-yeongyu/senpi/issues/2789)).
|
|
158
|
+
|
|
159
|
+
### Removed
|
|
160
|
+
|
|
161
|
+
## [2026.10.10-4] - 2026-10-06
|
|
162
|
+
|
|
163
|
+
### Breaking Changes
|
|
164
|
+
|
|
165
|
+
### Added
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
|
|
171
|
+
- A detached eval cell that waits for its kernel to start now says so (`waiting for the js kernel to be ready`) instead of `queued behind in the js kernel` with an empty predecessor ([#2790](https://github.com/code-yeongyu/senpi/issues/2790)).
|
|
172
|
+
|
|
173
|
+
- A stopped (or failed) detached cell's result and notification show its buffered output instead of the live `1/1 cells running` frame, so a cancelled cell no longer reads as still running. The kernel-state note now says plainly whether the kernel was restarted: `The JavaScript worker was not restarted; variables from earlier cells are kept.` instead of `... remains running; its existing variables are preserved.`, or `The JavaScript worker was restarted; variables from earlier cells are lost.` instead of `... was unresponsive to interrupt and was restarted ...` (the worker may have answered the interrupt and still needed a restart) ([#2791](https://github.com/code-yeongyu/senpi/issues/2791)).
|
|
174
|
+
|
|
175
|
+
- An isolated (`isolate: true`) eval cell's result names QuickJS as its runtime (`quickjs <version>, sandbox`) instead of the persistent kernel's Bun or Node runtime ([#2811](https://github.com/code-yeongyu/senpi/issues/2811)).
|
|
176
|
+
|
|
177
|
+
### Removed
|
|
178
|
+
|
|
179
|
+
## [2026.10.10-3] - 2026-10-06
|
|
180
|
+
|
|
181
|
+
### Breaking Changes
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
|
|
185
|
+
- An opt-in process-isolated JavaScript kernel (`isolation.js: "process"` or `SENPI_CODEMODE_JS_ISOLATION=process`) runs each JavaScript kernel in its own subprocess instead of a worker thread, so a kernel crash (`SIGSEGV`, out-of-memory, `process.exit`, an uncaught error) can no longer take down the host session; the next cell runs on a replacement child with a restart notice naming the crash. It isolates crashes, not hostile code: a cell is trusted as in worker mode, and hostile code belongs in `isolate: true` sandbox cells ([#2752](https://github.com/code-yeongyu/senpi/issues/2752) tracks hostile-cell isolation). The child runs on the host's own runtime, exits as soon as its host is gone (including `SIGKILL`), carries large output and `BigInt`/`undefined` values as worker mode does, and its frames carry a per-process token so stray output is never taken for a frame. The default stays `"worker"` and worker-mode behaviour is unchanged ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
|
|
189
|
+
- The README now documents every eval surface (helpers, magic cells, settings, JavaScript isolation modes and sandbox cells), and CI checks it against the helper census, so a new helper cannot ship undocumented ([#2787](https://github.com/code-yeongyu/senpi/pull/2787)).
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
### Removed
|
|
194
|
+
|
|
195
|
+
## [2026.10.10-2] - 2026-10-05
|
|
196
|
+
|
|
197
|
+
### Breaking Changes
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- `@code-yeongyu/senpi-codemode/executable-settings.json` lists the settings that name an executable run at session start (today `languages.pyInterpreter`). senpi's project-trust check reads it, so a project codemode file that sets one asks for trust, and a test fails if a new free-form string setting is neither on the list nor marked as not naming an executable ([#2772](https://github.com/code-yeongyu/senpi/pull/2772)).
|
|
202
|
+
|
|
203
|
+
### Changed
|
|
204
|
+
|
|
205
|
+
### Fixed
|
|
206
|
+
|
|
207
|
+
- Settings that were accepted but did nothing now take effect ([#2763](https://github.com/code-yeongyu/senpi/issues/2763)): `kernelTools.enabled: false` makes JavaScript `tool(fn)` and Python `@tool` refuse with `tools_unavailable`; `languages.pyInterpreter` makes the Python kernel run exactly that executable (a path that does not answer makes Python unavailable, with a warning naming the setting; one named by a project's own settings file is honored only in a trusted project); `prompt.advertiseHelpers: true` adds one line pointing at `tool_schema('eval:helpers')` to the eval description. Settings-file warnings (an unknown key, a fallback to defaults) now reach the user as a notice, or on stderr without a UI.
|
|
208
|
+
|
|
209
|
+
### Removed
|
|
210
|
+
|
|
15
211
|
## [2026.10.10] - 2026-10-05
|
|
16
212
|
|
|
17
213
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -59,6 +59,75 @@ task-tool names are known.
|
|
|
59
59
|
A missing optional interpreter removes that language from the session's `eval`
|
|
60
60
|
schema; it is not an installation failure.
|
|
61
61
|
|
|
62
|
+
### JavaScript process isolation
|
|
63
|
+
|
|
64
|
+
By default the JavaScript kernel runs as a worker thread on the host's own
|
|
65
|
+
runtime (`isolation.js: "worker"`). Setting `isolation.js: "process"` (or
|
|
66
|
+
`SENPI_CODEMODE_JS_ISOLATION=process`, which wins over the file) runs each
|
|
67
|
+
JavaScript kernel in its own subprocess instead, so a kernel crash (a native
|
|
68
|
+
`SIGSEGV`, an out-of-memory, `process.exit`, an uncaught error) cannot take down
|
|
69
|
+
the host session. The next cell runs on a replacement child, and its result
|
|
70
|
+
carries the restart notice and names the crash.
|
|
71
|
+
|
|
72
|
+
**Trust model.** Process mode isolates crashes, not hostile code. A cell runs in
|
|
73
|
+
the same process that talks to the host, so it can reach everything that process
|
|
74
|
+
holds, exactly as a worker-mode cell can. The frame token described below guards
|
|
75
|
+
only against accidental corruption of the channel. Hostile code belongs in
|
|
76
|
+
`isolate: true` sandbox cells. Hostile-cell isolation for process mode is tracked
|
|
77
|
+
in [#2752](https://github.com/code-yeongyu/senpi/issues/2752).
|
|
78
|
+
|
|
79
|
+
**Core dumps (Linux).** The kernel child marks itself non-dumpable at start
|
|
80
|
+
(`prctl(PR_SET_DUMPABLE, 0)`). A Linux system that pipes core dumps to
|
|
81
|
+
`systemd-coredump` or apport otherwise holds a crashed child until its dump is
|
|
82
|
+
read, which froze the cell for about 30 s; without the dump the crash is reported
|
|
83
|
+
at once, still naming its signal. Two side effects: no core file is written for a
|
|
84
|
+
crash of the kernel child itself, and a same-user debugger cannot attach to it
|
|
85
|
+
(`ptrace` needs a dumpable process). A cell's own subprocesses are unaffected,
|
|
86
|
+
because `exec` resets the flag. Set `SENPI_KERNEL_CORE_DUMPS=1` to keep dumps and
|
|
87
|
+
debugger access while debugging the kernel.
|
|
88
|
+
|
|
89
|
+
How it works:
|
|
90
|
+
|
|
91
|
+
- **Runtime.** The child runs on the host's own runtime: bun when senpi runs on
|
|
92
|
+
Bun, node otherwise. A compiled binary whose executable is neither falls back to
|
|
93
|
+
`bun` or `node` on `PATH`, preferring the host's kind. The badge names the
|
|
94
|
+
runtime that actually runs the child, for example `js (bun 1.4.x, process)`.
|
|
95
|
+
- **Frames.** The host writes a random token as the child's first line on fd 0,
|
|
96
|
+
and every frame the child sends carries it. The host parses only lines that
|
|
97
|
+
carry the token; any other line (a cell's raw fd write, a child process's
|
|
98
|
+
output) is delivered as text. Frames are written in full. Large text is split
|
|
99
|
+
below the 10 MiB frame limit, and a result too large to return fails its cell,
|
|
100
|
+
not the kernel. A `BigInt` or an `undefined` field reaches the host as it does
|
|
101
|
+
in worker mode.
|
|
102
|
+
- **File descriptors.** Under Bun, the control channel moves to a private
|
|
103
|
+
duplicate of fd 0, fd 0 becomes `/dev/null`, and fd 1 is re-pointed at a
|
|
104
|
+
pipe that a reader thread turns into `text` frames as it is written. The pipe is kept
|
|
105
|
+
blocking (a child that switches it to non-blocking is undone before every cell and around
|
|
106
|
+
every child process a cell starts),
|
|
107
|
+
so output is never held in memory while a cell is busy. A cell's direct
|
|
108
|
+
`process.stdout.write` or fd 1 write (or a child process inheriting fd 1)
|
|
109
|
+
arrives before that cell's result. In a bun child, raw fd 1 bytes are not
|
|
110
|
+
interleaved with `console` output in the order they were written. A node
|
|
111
|
+
child cannot move its fds: cell output shares the channel, and fd 0 is the
|
|
112
|
+
channel. If libc cannot be loaded through `bun:ffi`, a bun child runs
|
|
113
|
+
the same way and still starts; the first cell's stderr says why.
|
|
114
|
+
- **Lifetime.** The child exits as soon as its control channel closes. That
|
|
115
|
+
covers every way the host ends, including `SIGKILL`. A watchdog thread also
|
|
116
|
+
compares the parent pid with the one recorded at start, so a child whose cell
|
|
117
|
+
never yields (a busy loop, a blocking call) still ends with its host, including
|
|
118
|
+
under a Linux subreaper. A process-mode kernel never outlives its host.
|
|
119
|
+
- **Failed start.** There is no inline fallback in process mode. A failed start
|
|
120
|
+
settles the waiting cell with a capability-gap result naming the missing
|
|
121
|
+
runtime (`Install bun or node, or use isolation.js: "worker"`). The result's
|
|
122
|
+
memory reading is the child's process footprint.
|
|
123
|
+
|
|
124
|
+
The default stays `"worker"`, and worker-mode behaviour is unchanged.
|
|
125
|
+
|
|
126
|
+
To run the `environments` suites with bun children (the runtime the product
|
|
127
|
+
uses), run them under bun: `SENPI_CODEMODE_JS_ISOLATION=process bunx --bun vitest
|
|
128
|
+
run test/environments`. Under plain `vitest` the children follow the test
|
|
129
|
+
runner's runtime, which is node.
|
|
130
|
+
|
|
62
131
|
Python startup waits for the interpreter's `ready` event. It reports progress
|
|
63
132
|
through `stdlib-imports`, `runtime-init`, and `host-init`; advancing to the next
|
|
64
133
|
stage resets an inactivity guard rather than consuming a total startup budget.
|
|
@@ -254,7 +323,7 @@ Configuration is loaded in this order:
|
|
|
254
323
|
| `memory.retainedResultsMb` | `32` | In-memory byte budget (MiB) for the settled cells kept for `peek`/`list`, on top of the 32-cell count cap; the oldest go first and the newest is always kept. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_RESULTS_MB` (a non-negative integer). |
|
|
255
324
|
| `memory.retainedImagesMb` | `256` | Disk budget (MiB) for settled-cell images. Images of settled cells (foreground and detached) are written as base64 files under `<session artifacts>/settled-images/` instead of staying in memory, and `peek` reads them back, so it returns the full result. Beyond the budget the oldest files are deleted first; an evicted cell's files are deleted with it; the directory is removed when the session ends. A `peek` whose image file is gone returns the text plus a one-line note. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_IMAGES_MB` (a non-negative integer). |
|
|
256
325
|
| `memory.idleParkMinutes` | `0` | Off by default. When greater than 0, a kernel with no cell running or queued for this many minutes is closed to give its memory back, and the next cell for that language starts a fresh one; that cell's result says the kernel was restarted and every earlier global is lost. Applies to every language. |
|
|
257
|
-
| `languages.pyInterpreter` | unset | Explicit Python interpreter path. Unset keeps
|
|
326
|
+
| `languages.pyInterpreter` | unset | Explicit Python interpreter path: the Python kernel runs exactly that executable (a path with spaces is fine). A path that does not answer `--version` makes Python unavailable for the session, with a warning naming the setting. Unset keeps `PATH` detection. |
|
|
258
327
|
| `environments.managedRoot` | unset | Root directory for managed per-session environments; unset uses the session's default location. |
|
|
259
328
|
| `environments.autoProvision` | `true` | Lets installs provision a managed environment. `false` makes installs refuse with `environment_installer_unavailable`. |
|
|
260
329
|
| `environments.js.installer` | `auto` | Installer for the managed JavaScript environment: `auto`, `bun` or `npm`. |
|
|
@@ -264,16 +333,16 @@ Configuration is loaded in this order:
|
|
|
264
333
|
| `sandbox.memoryMb` | `64` | Memory cap (MiB) for a sandbox cell. Env override: `SENPI_CODEMODE_SANDBOX_MEMORY_MB` (a positive integer wins). |
|
|
265
334
|
| `sandbox.timeoutSeconds` | `300` | Time limit for a sandbox cell. |
|
|
266
335
|
| `prompt.advertiseHelpers` | `false` | When `true`, one pointer line to `tool_schema('eval:helpers')` is appended to the eval description. |
|
|
267
|
-
| `kernelTools.enabled` | `true` | Allows cells to define kernel tools
|
|
336
|
+
| `kernelTools.enabled` | `true` | Allows cells to define kernel tools. `false` makes JavaScript `tool(fn)` and Python `@tool` refuse with `tools_unavailable`, naming this setting. |
|
|
268
337
|
|
|
269
|
-
|
|
338
|
+
Every key in the table takes the effect its row describes.
|
|
270
339
|
|
|
271
340
|
`SENPI_CODEMODE_PY`, `SENPI_CODEMODE_JS`, `SENPI_CODEMODE_RB`, and
|
|
272
341
|
`SENPI_CODEMODE_JL` override the corresponding file setting. `1` or `true`
|
|
273
342
|
enables; `0` or `false` disables. Any other value leaves the file setting in
|
|
274
343
|
effect.
|
|
275
344
|
|
|
276
|
-
A top-level key this version does not know is ignored with one warning naming it, and the rest of the file still applies, so a settings file written for a newer senpi keeps working. Malformed JSON, or an invalid value or an unknown key inside a known setting, falls back to all defaults with a warning.
|
|
345
|
+
Settings problems reach the user as a notice in the interactive UI, or a `[senpi-codemode]` line on stderr without one. A top-level key this version does not know is ignored with one warning naming it, and the rest of the file still applies, so a settings file written for a newer senpi keeps working. Malformed JSON, or an invalid value or an unknown key inside a known setting, falls back to all defaults with a warning.
|
|
277
346
|
The detached-cell environment override uses the run-budget parser: a positive
|
|
278
347
|
base-10 integer wins over the file value; zero, negative, and malformed values
|
|
279
348
|
leave the file value in effect.
|
|
@@ -288,18 +357,22 @@ options object and asynchronous helpers are `await`-able.
|
|
|
288
357
|
| --- | --- |
|
|
289
358
|
| `display(value)` | Emits text, structured JSON, markdown, or image display data. Images reach the model only through `display`: pass a figure, raw image bytes (PNG/JPEG/GIF/WebP/BMP sniffed), a `data:` URL, a `Blob`-like or `Bun.Image` value, a marshalled tool result, or one of its `images[i]` frames. |
|
|
290
359
|
| `print(value, ...)` | Emits text output. |
|
|
360
|
+
| `text(value)` (rb, jl) | Emits `value` as one text item without separators (`print` joins its arguments and calls it). |
|
|
361
|
+
| `display_image(base64, mime_type)` (rb, jl) | Emits an image from base64 data; `mime_type` defaults to `image/png` (Ruby: `mime_type:` keyword). |
|
|
291
362
|
| `read(path, offset?, limit?)` | Reads text with 1-indexed line slicing. `local://` paths resolve under the session artifact root. |
|
|
292
363
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
293
364
|
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
294
365
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
295
|
-
| `
|
|
366
|
+
| `packages.install(manager, requirements, {timeout?})` (js, py) | The `%pip install` (py, manager `"pip"`) or `%bun add` / `%npm add` (js, `"bun"` / `"npm"`) installer as a call: same session environment, receipt (`py`: manager, mode, root, revision, requested, resolved, changed; `js`: installer, mode, revision, added, shadowed), cancellation by `stop` and `environment_install_*` codes; `timeout` defaults to 600 seconds and fails with `environment_install_timeout`. Python: `packages.install("pip", [...], timeout=...)`. |
|
|
367
|
+
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. Five virtual entries document codemode itself on demand and are never listed: `eval:wait`, `eval:helpers`, `eval:kernel-tools`, `eval:environments` (the `%pip`, `%bun`/`%npm`, `%environment` and `%load` magics, their results and error codes) and `eval:isolation` (`isolate: true` cells, their limits and error codes). |
|
|
368
|
+
| `require(specifier)`, `createRequire(path)` (js) | CommonJS loading inside a cell, resolved like `import`: builtins (with or without `node:`), relative files and JSON from the cell's directory, bare packages from the session's project and then its managed package environment (`%bun add`/`%npm add`). A missing module raises the runtime's own `MODULE_NOT_FOUND` error. Like Node's `require`, it carries `require.resolve` (the same lookup the call uses, so both name the same copy; builtins resolve to the runtime's own id), `require.resolve.paths` and `require.cache` (the runtime's CommonJS cache). |
|
|
296
369
|
| `tool(fn, metadata?)` (js) | Registers a named function as a kernel tool for in-process children. `metadata.name` registers it under that name instead of the function's; arguments are still passed in the function's parameter order. |
|
|
297
370
|
| `tool.defined()` / `tool.undefine(name)` (js) | List the kernel tools this kernel defines (sorted), and remove one (`true` if it existed). A descriptor taken before `undefine` can no longer be invoked. Both names are reserved: a kernel tool can't be registered as `defined` or `undefine` (`reserved_tool_name`), and a host tool with either name is shadowed in the `tool` namespace, so it can't be called from a JavaScript cell. |
|
|
298
371
|
| `@tool` / `@tool(name=, description=, schema=)` (py) | Registers a Python function as a kernel tool for in-process children; the schema is inferred from its type hints (`tool_schema("eval:kernel-tools")` lists the rules). Callbacks run while the kernel is idle or its cell is parked in a host call, never in the middle of a running computation. `tool.defined()` and `tool.undefine(name)` work as in JavaScript. Ruby and Julia kernels answer `tools_unavailable`. |
|
|
299
372
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
|
300
373
|
| `agent(prompt, ...)` | Delegates to the configured active `taskTools.task` tool. Supports background handles and structured JSON results. |
|
|
301
374
|
| `wait(handles, timeout?, mode?)` | Blocks the cell until the given handles settle (agent handle records, `handle()` views, completion handles, closed workpools, or saved `{kind, id, run_epoch}` references). `mode` is `all` (values in input order; the first failed, cancelled, or lost handle raises), `any` (`{index, ref, value}` of the first success), or `settled` (one outcome per input slot). `timeout` is wall-clock seconds from entry; on expiry `eval_wait_timeout` is raised and nothing is cancelled. Rides the bridge-call path, so the run budget pauses while parked. Agent and workpool handles need the host's `EvalHandleHost` capability (`eval_wait_unavailable` without it); completion handles always work. Julia extends `Base.wait` for handle views (`wait(handle(node))`). Details: `tool_schema("eval:wait")`. |
|
|
302
|
-
| `handle(node \| ref \| {pool_id})` | Returns a rich view: the legacy record's fields plus a non-enumerable `control` (Python: attribute on a `dict` subclass; Ruby: singleton method) with
|
|
375
|
+
| `handle(node \| ref \| {pool_id})` | Returns a rich view: the legacy record's fields plus a non-enumerable `control` (Python: attribute on a `dict` subclass; Ruby: singleton method) with `.status()`, `.output(format?, offset?, limit?)`, `.send(message)` (agent handles only), `.cancel()` (idempotent for that run epoch; never touches a successor run), and `.wait(timeout?)`. Every control call is fenced by owner, id, and `run_epoch` inside the task owner (`eval_handle_stale`, `eval_handle_forbidden`). `completion(prompt, handle: true)` returns such a view for a host completion. The `agent(..., handle: true)` record itself is unchanged. Details: `tool_schema("eval:helpers")`. |
|
|
303
376
|
| `workpool(agent, name, mode?, tools?)` | Creates a thin adapter over the normal host `workpool` tool; exposes `pool_id`, `push(items)`, `close()`, `inspect()`, and `cancel()`. JS awaits creation and operations. `tools` is a list of kernel-tool names this cell defined; the pool's workers may call exactly those, the host refuses any name the caller doesn't hold, and a value that isn't a list of names raises `invalid_tools`. Ruby and Julia define no kernel tools, so they have nothing to grant. |
|
|
304
377
|
| `output(ids, format?, offset?, limit?)` | Delegates transcript retrieval to the configured active `taskTools.output` tool. |
|
|
305
378
|
| `parallel(thunks)` | Runs thunks through the configured bounded pool while preserving input order. |
|
|
@@ -394,6 +467,10 @@ free; otherwise it settles `cancelled` with `eval_background_capacity_reached`,
|
|
|
394
467
|
listing the live cells and a stop-or-wait remedy. Cells that complete inside the
|
|
395
468
|
window return normally. Cancelling a queued cell never interrupts its predecessor.
|
|
396
469
|
Do not re-run a detached or queued cell; each detached cell completes as one notification.
|
|
470
|
+
The notification carries the same text the cell's result would have shown in the
|
|
471
|
+
foreground (the output sink's configured head, its tail, the middle-elision marker and
|
|
472
|
+
the full-output artifact notice), framed by the outcome line and the kernel-state note,
|
|
473
|
+
plus any images the cell displayed.
|
|
397
474
|
|
|
398
475
|
Queued steering also detaches an eligible interactive foreground call, including
|
|
399
476
|
one paused in a host tool bridge, without cancelling its computation. If the
|
|
@@ -434,11 +511,29 @@ Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
|
|
|
434
511
|
`eval({ action: "stop", cell_id })` to cancel it. Stopping a queued cell removes it
|
|
435
512
|
without interrupting the active cell; kernel state is retained. Python running-cell stop interrupts the
|
|
436
513
|
existing kernel and preserves variables. JavaScript stop is cooperative first:
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
514
|
+
Stop ends the cell and everything it started, not the kernel's variables. The
|
|
515
|
+
worker rejects the cell's pending bridge `tool.*` calls and releases the cell:
|
|
516
|
+
its timers are cleared (the globals and the `node:timers` module alike), its
|
|
517
|
+
pending `Bun.sleep`, `node:timers/promises` waits, `fetch` requests and file
|
|
518
|
+
reads reject with the interruption, and the sockets, servers, file streams,
|
|
519
|
+
`readline` interfaces, WebSockets, WebViews, nested workers and child processes
|
|
520
|
+
it opened are closed or terminated, whether made with a factory or a
|
|
521
|
+
constructor. A resource that will not close is reported on the cell output. The stop result arrives once those children are gone, and the
|
|
522
|
+
worker and every global from earlier cells survive. If the stopped cell's own
|
|
523
|
+
`catch`/`finally` still runs, it cannot print, call tools, start processes,
|
|
524
|
+
schedule timers or open connections. One boundary remains: code that resumes
|
|
525
|
+
because a later cell resolves a promise the stopped cell was awaiting runs
|
|
526
|
+
until it reaches an operation a stopped cell may no longer start (output, tool
|
|
527
|
+
calls, processes, timers, network, files, message channels, workers).
|
|
528
|
+
|
|
529
|
+
An unhandled promise rejection never crashes the JavaScript kernel; its
|
|
530
|
+
variables are kept, as in the Node REPL and Jupyter. The rejection is reported
|
|
531
|
+
on the cell that is running, or on the next cell, naming the cell that started
|
|
532
|
+
the work when it is known ("from cell <id>, after it was stopped"), with the
|
|
533
|
+
error message and the top of its stack. A burst becomes one report plus
|
|
534
|
+
"... and K more unhandled promise rejections". A fatal error (an uncaught
|
|
535
|
+
exception) still restarts the worker, and the result says variables are lost. Only a cell stopped during a `Bun.$` command costs
|
|
536
|
+
the worker VM. A worker blocked in a synchronous call (`Bun.spawnSync`,
|
|
442
537
|
`child_process.spawnSync`) cannot be stopped at all; after a 3 s termination
|
|
443
538
|
deadline a fresh worker replaces it, the cell output gains a stderr line naming
|
|
444
539
|
the blocked synchronous call, and the blocked call keeps running until it
|
|
@@ -491,6 +586,13 @@ original sizes, then a plain-path notice such as
|
|
|
491
586
|
package.
|
|
492
587
|
- Task transcript formats are limited to full (`raw`) and trailing (`tail`)
|
|
493
588
|
output. Query, JSON, and stripped metadata formats are task-engine concerns.
|
|
589
|
+
- There is no `pool.wait()`: a workpool's aggregate is delivered by the host after
|
|
590
|
+
`close()`, so a kernel never blocks on another engine's workers.
|
|
591
|
+
- There is no `code_mode_only` mode and no speculative cell execution; both are
|
|
592
|
+
deferred to a separate design (omo#9232) rather than approximated here.
|
|
593
|
+
- Kernels are never shared across owners: each session (and each in-process child
|
|
594
|
+
with its own eval tool) owns its kernels, and handles are fenced by owner, id and
|
|
595
|
+
run epoch.
|
|
494
596
|
|
|
495
597
|
## Security and lifecycle
|
|
496
598
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.11",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
-
"./package.json": "./package.json"
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./executable-settings.json": "./src/config/executable-settings.json"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src",
|
|
@@ -32,15 +33,15 @@
|
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@babel/parser": "8.0.4",
|
|
35
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
36
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.11",
|
|
36
37
|
"quickjs-wasi": "3.6.2",
|
|
37
38
|
"typebox": "1.3.34"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.10.11"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
44
|
+
"@code-yeongyu/senpi": "2026.10.11"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|
|
46
47
|
"senpi",
|
|
@@ -24,7 +24,7 @@ export const kernelMemoryReportSchema = Type.Object({
|
|
|
24
24
|
approximate: Type.Optional(Type.Boolean()),
|
|
25
25
|
/** `liveBytes` was measured right after a full collection in this cell. */
|
|
26
26
|
gcRan: Type.Optional(Type.Boolean()),
|
|
27
|
-
/** Largest user globals, measured
|
|
27
|
+
/** Largest user globals, measured after collection or on demand when the host footprint reaches a threshold. */
|
|
28
28
|
globals: Type.Optional(Type.Array(kernelMemoryGlobalSchema, { maxItems: 8 })),
|
|
29
29
|
/** Host-set: live memory after collection reached the ceiling; the kernel restarts once its queue drains. */
|
|
30
30
|
overCeiling: Type.Optional(Type.Boolean()),
|
|
@@ -37,6 +37,8 @@ export const kernelMemoryReportSchema = Type.Object({
|
|
|
37
37
|
/** On-demand heap reading between cells (JS kernels): no cell runs and no collection is forced. */
|
|
38
38
|
export const kernelMemoryQueryHostToKernelSchemas = [
|
|
39
39
|
Type.Object({ type: Type.Literal("memory-query"), requestId: Type.String({ minLength: 1 }) }),
|
|
40
|
+
/** Ruby/Julia only: bounded, read-only globals sizing before the owning cell settles. */
|
|
41
|
+
Type.Object({ type: Type.Literal("memory-globals"), cellId: Type.String({ minLength: 1 }) }),
|
|
40
42
|
] as const;
|
|
41
43
|
|
|
42
44
|
export const kernelMemoryQueryKernelToHostSchemas = [
|
|
@@ -46,6 +48,11 @@ export const kernelMemoryQueryKernelToHostSchemas = [
|
|
|
46
48
|
liveBytes: Type.Integer({ minimum: 0 }),
|
|
47
49
|
measure: Type.Literal("heap"),
|
|
48
50
|
}),
|
|
51
|
+
Type.Object({
|
|
52
|
+
type: Type.Literal("memory-globals-result"),
|
|
53
|
+
cellId: Type.String({ minLength: 1 }),
|
|
54
|
+
globals: Type.Array(kernelMemoryGlobalSchema, { maxItems: 8 }),
|
|
55
|
+
}),
|
|
49
56
|
] as const;
|
|
50
57
|
|
|
51
58
|
export type KernelMemoryThresholds = Static<typeof kernelMemoryThresholdsSchema>;
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -101,7 +101,7 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
101
101
|
const kernelStateSchema = Type.Union([Type.Literal("lost"), Type.Literal("restarted"), Type.Literal("not-run")]);
|
|
102
102
|
|
|
103
103
|
const kernelToHostMessageSchema = Type.Union([
|
|
104
|
-
Type.Object({ type: Type.Literal("ready") }),
|
|
104
|
+
Type.Object({ type: Type.Literal("ready"), memoryGlobals: Type.Optional(Type.Boolean()) }),
|
|
105
105
|
Type.Object({ type: Type.Literal("init-failed"), error: bridgeErrorSchema }),
|
|
106
106
|
Type.Object({
|
|
107
107
|
type: Type.Literal("text"),
|
|
@@ -237,10 +237,15 @@ export function parseBridgeJsonLine(line: string, options: BridgeFrameOptions =
|
|
|
237
237
|
export function decodeBridgeFrame(line: string, options: BridgeFrameOptions = {}): BridgeMessageDecodeResult {
|
|
238
238
|
const parsed = parseBridgeJsonLine(line, options);
|
|
239
239
|
if (!parsed.ok) return parsed;
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
return validateBridgeMessage(parsed.value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Checks an already-parsed frame against the bridge schema (for transports that transform values before checking). */
|
|
244
|
+
export function validateBridgeMessage(value: unknown): BridgeMessageDecodeResult {
|
|
245
|
+
if (Value.Check(bridgeMessageSchema, value)) {
|
|
246
|
+
return { ok: true, message: value };
|
|
242
247
|
}
|
|
243
|
-
const firstError = Value.Errors(bridgeMessageSchema,
|
|
248
|
+
const firstError = Value.Errors(bridgeMessageSchema, value)[0];
|
|
244
249
|
const message = firstError ? `Invalid bridge message: ${firstError.message}` : "Invalid bridge message";
|
|
245
250
|
return { ok: false, error: { code: "invalid_message", message } };
|
|
246
251
|
}
|
package/src/bridge/reserved.ts
CHANGED
|
@@ -15,6 +15,8 @@ export const RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__" as const;
|
|
|
15
15
|
export const RESERVED_HANDLE_SEND_TOOL = "__handle_send__" as const;
|
|
16
16
|
/** `handle(node).control.cancel()`: idempotent for its epoch; a successor run is never touched. */
|
|
17
17
|
export const RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__" as const;
|
|
18
|
+
/** In-cell `packages.install(manager, requirements, {timeout?})`: the `%pip` / `%bun` / `%npm` installer as a call. */
|
|
19
|
+
export const RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__" as const;
|
|
18
20
|
/** Canonical oh-my-pi eval-timeout pause operation. */
|
|
19
21
|
export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
|
|
20
22
|
/** Canonical oh-my-pi eval-timeout resume operation. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EvalSchemaResult } from "./schema-bridge.ts";
|
|
2
|
+
|
|
3
|
+
export type NamedEvalSchema = Extract<EvalSchemaResult, { readonly name: string }>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The on-demand documentation for package environments and isolated cells (`tool_schema("eval:environments")` and
|
|
7
|
+
* `tool_schema("eval:isolation")`). Like the other virtual entries they are never listed, never run anything, and add
|
|
8
|
+
* nothing to the eval prompt or input schema. Every magic, code and message named here is the one the source emits.
|
|
9
|
+
*/
|
|
10
|
+
export const environmentsEntry: NamedEvalSchema = {
|
|
11
|
+
name: "eval:environments",
|
|
12
|
+
description: [
|
|
13
|
+
"Host magic cells: a cell whose first code line (blank and comment lines skipped) is one of these runs on the host, in the language kernel's queue, so it orders with ordinary cells and a queued one is removed by stop like any queued cell. A magic followed by more code is refused: put it on its own cell, then run the code that uses it in the next cell.",
|
|
14
|
+
"py: %pip install <requirements ...> installs into the session's environment (a trailing backslash continues the line). Result: installed <packages> into <mode> (revision <n>); already-imported modules stay cached until reset.",
|
|
15
|
+
"js: %bun add <packages ...> or %npm add <packages ...> (install is accepted as a synonym of add). Result: added <packages> with <installer> into <mode>[ (revision <n>)]; already-imported modules stay cached until reset. A package the project's own node_modules still shadows is named with environment_resolution_conflict in the result.",
|
|
16
|
+
"py and js: %environment managed | project selects where installs go. managed (the default) uses per-session revisions under the session's artifacts directory (or environments.managedRoot) and never touches project files; project installs into the session directory (py: .senpi/python-packages; js: the project root).",
|
|
17
|
+
"py and js: %load <file> runs a local file as a cell, read when its turn comes in the queue, with the file's name in tracebacks; definitions persist like any cell. It accepts a path relative to the session directory, local:// or file://, reads files up to 8 MiB and never fetches remote URLs.",
|
|
18
|
+
"Installs pause the run budget like a bridge call, stream the installer's output into the cell, detach past the foreground window like any cell, and are stopped by stop.",
|
|
19
|
+
'js and py: packages.install(manager, requirements, {timeout?}) runs the same install as a call instead of a magic: manager "pip" in a Python cell, "bun" or "npm" in a JavaScript cell; requirements is a non-empty string or a non-empty list of non-empty strings, and timeout is a positive number of seconds (default 600; py spells it packages.install("pip", [...], timeout=...)). pip receives the requirement list as separate arguments, never re-split; a js package spec cannot contain whitespace. It drives the same session environment the magic uses, returns that install\'s receipt (py: manager, mode, root, revision, requested, resolved, changed; js: installer, mode, revision, added, shadowed) and is cancelled by stop like a magic install. Unlike the magic, the installer\'s output is not streamed into the cell: the receipt is the result, and a failure carries the installer\'s stderr tail.',
|
|
20
|
+
"Install failures are cell errors that start with their code: environment_install_failed (an argument the host rejects: an unknown manager, an empty requirement list, a bun/npm requirement with whitespace or a non-positive timeout; or an installer failure, with the installer's stderr tail. A JS options value that is not an object, or a Python timeout that float() cannot convert, fails earlier in the cell with a plain TypeError or ValueError), environment_install_timeout (a packages.install() still running after its timeout, default 600 s, was stopped), environment_install_cancelled (stopped before or during the install), environment_installer_unavailable (no interpreter or no pip; no JavaScript package environment in this session; neither bun nor npm on PATH, or the chosen one missing; the installer failed to start; installs turned off with environments.autoProvision: false; or pip asked of a session with no Python interpreter, or bun/npm asked of a Python cell), environment_resolution_conflict (py: pip reported a dependency conflict). A %load failure, and a magic refused because more code follows it, are plain cell errors without a code.",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
parameters: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
"%pip": { description: "py: %pip install <requirements ...>" },
|
|
26
|
+
"%bun": { description: "js: %bun add <packages ...>" },
|
|
27
|
+
"%npm": { description: "js: %npm add <packages ...>" },
|
|
28
|
+
"%environment": { enum: ["managed", "project"] },
|
|
29
|
+
"%load": { description: "py and js: %load <path | local://... | file://...>" },
|
|
30
|
+
"packages.install": {
|
|
31
|
+
description: "js and py: packages.install(manager, requirements, {timeout?}) (py: timeout=...)",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const isolationEntry: NamedEvalSchema = {
|
|
38
|
+
name: "eval:isolation",
|
|
39
|
+
description: [
|
|
40
|
+
"isolate: true runs one JavaScript cell in a fresh QuickJS sandbox instead of the persistent kernel. It is available only when sandbox.enabled is on in the codemode settings; only then does the eval input schema carry the isolate field. An older senpi drops the field and would run the code as an ordinary cell, so check this entry before relying on it.",
|
|
41
|
+
'Requests are refused with eval_isolate_invalid when sandbox cells are turned off, when language is not "js", when isolate is not a boolean, with reset: true (a sandbox cell always starts fresh), or when eval runs through a proxy that cannot isolate a cell.',
|
|
42
|
+
"An isolated cell shares nothing with the kernel. Its globals are print, display, text, image, console, exit, tool.<name>(args) (also reachable as tools), ALL_TOOLS (the names and descriptions of the tools it may call), load and store. Its tool calls go through the host, so permission hooks still apply, and tool calls still pending when the script settles are aborted. The store is rejected: load returns nothing and store fails with eval_isolate_no_state.",
|
|
43
|
+
"Limits: sandbox.timeoutSeconds (default 300) and sandbox.memoryMb (default 64; SENPI_CODEMODE_SANDBOX_MEMORY_MB overrides it). The result's runtime names quickjs and its version, with isolation sandbox.",
|
|
44
|
+
"Failures start with their code: eval_isolate_timeout, eval_isolate_aborted, eval_isolate_memory_limit, eval_isolate_unresolved_promise (the script ended with a promise still pending), eval_isolate_no_state, eval_isolate_unavailable (the sandbox itself failed: it could not load, or its worker crashed, exited or lost its output; if that happened after the script started, tool calls the cell made may already have run, so check their effects before retrying).",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
isolate: { type: "boolean", description: 'true with language: "js" runs the cell in a fresh sandbox' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { environmentsEntry, isolationEntry, type NamedEvalSchema } from "./eval-environment-schemas.ts";
|
|
1
2
|
import type { EvalSchemaResult } from "./schema-bridge.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -22,7 +23,7 @@ const handleRefSchema = {
|
|
|
22
23
|
required: ["kind", "id", "run_epoch"],
|
|
23
24
|
} as const;
|
|
24
25
|
|
|
25
|
-
const waitEntry:
|
|
26
|
+
const waitEntry: NamedEvalSchema = {
|
|
26
27
|
name: "eval:wait",
|
|
27
28
|
description: [
|
|
28
29
|
"wait(handles, {timeout?, mode?}) blocks the cell until the given handles settle (js: await wait(...); py/rb: wait(...); jl: wait(handle(node)) or wait([handle(a), handle(b)])).",
|
|
@@ -44,7 +45,7 @@ const waitEntry: EvalSchemaResult = {
|
|
|
44
45
|
},
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
const helpersEntry:
|
|
48
|
+
const helpersEntry: NamedEvalSchema = {
|
|
48
49
|
name: "eval:helpers",
|
|
49
50
|
description: [
|
|
50
51
|
"Advanced cell helpers (all four languages; js helpers are awaited).",
|
|
@@ -70,7 +71,7 @@ const helpersEntry: EvalSchemaResult = {
|
|
|
70
71
|
},
|
|
71
72
|
};
|
|
72
73
|
|
|
73
|
-
const kernelToolsEntry:
|
|
74
|
+
const kernelToolsEntry: NamedEvalSchema = {
|
|
74
75
|
name: "eval:kernel-tools",
|
|
75
76
|
description: [
|
|
76
77
|
"Kernel tools: functions a cell defines that in-process children can call (js and py; rb/jl answer tools_unavailable).",
|
|
@@ -90,12 +91,18 @@ const kernelToolsEntry: EvalSchemaResult = {
|
|
|
90
91
|
},
|
|
91
92
|
};
|
|
92
93
|
|
|
93
|
-
const VIRTUAL_ENTRIES: ReadonlyMap<string, EvalSchemaResult> = new Map([
|
|
94
|
+
const VIRTUAL_ENTRIES: ReadonlyMap<string, EvalSchemaResult> = new Map<string, EvalSchemaResult>([
|
|
94
95
|
[waitEntry.name, waitEntry],
|
|
95
96
|
[helpersEntry.name, helpersEntry],
|
|
96
97
|
[kernelToolsEntry.name, kernelToolsEntry],
|
|
98
|
+
[environmentsEntry.name, environmentsEntry],
|
|
99
|
+
[isolationEntry.name, isolationEntry],
|
|
97
100
|
]);
|
|
98
101
|
|
|
99
102
|
export function virtualEvalSchema(name: string): EvalSchemaResult | undefined {
|
|
100
103
|
return VIRTUAL_ENTRIES.get(name);
|
|
101
104
|
}
|
|
105
|
+
|
|
106
|
+
export function virtualEvalSchemaNames(): readonly string[] {
|
|
107
|
+
return [...VIRTUAL_ENTRIES.keys()];
|
|
108
|
+
}
|