@code-yeongyu/senpi-codemode 2026.10.9 → 2026.10.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 +190 -0
- package/README.md +161 -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/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -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/environments/revision-store.ts +20 -4
- 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 +51 -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 +3 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/runner.jl +11 -22
- package/src/kernels/js/cell-dispatch.ts +44 -0
- 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 +46 -51
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/host-entries.ts +62 -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 +25 -6
- 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 +53 -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-package-resolve.js +153 -0
- 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/host-cell.ts +8 -31
- 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 +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/bun-ffi.ts +24 -0
- package/src/kernels/shared/host-cell.ts +57 -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 +12 -11
- 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-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/eval-tool.ts +19 -5
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +18 -3
- package/src/tool/magic-cell-host.ts +60 -2
- package/src/tool/magic-cells.ts +12 -2
- 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 +53 -16
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +34 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HostToKernelMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
4
|
+
import { refusedEntry } from "./host-entries.ts";
|
|
5
|
+
import {
|
|
6
|
+
type JavaScriptKernelOptions,
|
|
7
|
+
type JavaScriptRunInput,
|
|
8
|
+
resolveKernelToolNameSource,
|
|
9
|
+
} from "./kernel-contract.ts";
|
|
10
|
+
import type { LocalModuleLoader } from "./local-module-loader.ts";
|
|
11
|
+
|
|
12
|
+
export type CellDispatch =
|
|
13
|
+
| { readonly kind: "host"; readonly host: HostCellExecutor }
|
|
14
|
+
| { readonly kind: "worker"; readonly frames: readonly HostToKernelMessage[] };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decides at the cell's turn how it runs: as a host entry (an install, or a turn-time refusal such as an unreadable
|
|
18
|
+
* `%load` file) or in the worker, with the frames the worker needs (the current kernel-tool names, then the cell).
|
|
19
|
+
*/
|
|
20
|
+
export function dispatchCell(
|
|
21
|
+
queued: JavaScriptRunInput,
|
|
22
|
+
loader: LocalModuleLoader,
|
|
23
|
+
options: Pick<JavaScriptKernelOptions, "hostToolNames" | "foreignLanguageNames">,
|
|
24
|
+
): CellDispatch {
|
|
25
|
+
const input = inputAtStart(queued);
|
|
26
|
+
if ("refused" in input) return { kind: "host", host: refusedEntry(input.refused) };
|
|
27
|
+
if (input.host !== undefined) return { kind: "host", host: input.host };
|
|
28
|
+
return {
|
|
29
|
+
kind: "worker",
|
|
30
|
+
frames: [
|
|
31
|
+
{
|
|
32
|
+
type: "kernel-tools-names",
|
|
33
|
+
hostToolNames: resolveKernelToolNameSource(options.hostToolNames),
|
|
34
|
+
foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "run",
|
|
38
|
+
cellId: input.cellId,
|
|
39
|
+
code: loader.prepareCell(input.code, input.kernelPreludes, input.sourceFile, input.packageRoot?.()),
|
|
40
|
+
timeoutMs: input.timeoutMs,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { currentCell, onReleaseOf, settleWithCell } from "./cell-run-context.js";
|
|
2
|
+
|
|
3
|
+
// What a cell owns, and how each kind ends when the cell is stopped. Every patch is a no-op outside a cell, refuses
|
|
4
|
+
// (or never starts) new work for a released cell, and registers what a live cell starts so release can end it.
|
|
5
|
+
const RESOURCE_CLOSERS = ["terminate", "destroy", "stop", "close", "end"];
|
|
6
|
+
|
|
7
|
+
// Some host bindings are read-only (Bun.fetch); those keep their own behaviour and are not owned by cells.
|
|
8
|
+
function replace(owner, name, replacement, patched) {
|
|
9
|
+
const original = owner[name];
|
|
10
|
+
try {
|
|
11
|
+
owner[name] = replacement;
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (owner[name] !== replacement) return;
|
|
16
|
+
patched.push(() => {
|
|
17
|
+
owner[name] = original;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function builtin(name) {
|
|
22
|
+
return process.getBuiltinModule?.(name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function closeResource(resource) {
|
|
26
|
+
for (const method of RESOURCE_CLOSERS) {
|
|
27
|
+
if (typeof resource?.[method] !== "function") continue;
|
|
28
|
+
if (method === "stop") resource.stop(true);
|
|
29
|
+
else resource[method]();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ownResultsOf(owner, names, patched) {
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
const original = owner?.[name];
|
|
37
|
+
if (typeof original !== "function") continue;
|
|
38
|
+
const own = function (...args) {
|
|
39
|
+
const cell = currentCell();
|
|
40
|
+
if (cell === undefined) return original.apply(this, args);
|
|
41
|
+
if (cell.released) throw cell.interruption;
|
|
42
|
+
const result = original.apply(this, args);
|
|
43
|
+
const register = (resource) => {
|
|
44
|
+
onReleaseOf(cell, () => closeResource(resource));
|
|
45
|
+
return resource;
|
|
46
|
+
};
|
|
47
|
+
return result instanceof Promise ? result.then(register) : register(result);
|
|
48
|
+
};
|
|
49
|
+
Object.assign(own, original);
|
|
50
|
+
replace(owner, name, own, patched);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// `globals` is the global scope already patched: a module binding that is the same owned function is left alone.
|
|
55
|
+
function patchTimers(scope, patched, globals = undefined) {
|
|
56
|
+
for (const [name, clearName] of [
|
|
57
|
+
["setTimeout", "clearTimeout"],
|
|
58
|
+
["setInterval", "clearInterval"],
|
|
59
|
+
["setImmediate", "clearImmediate"],
|
|
60
|
+
]) {
|
|
61
|
+
const original = scope?.[name];
|
|
62
|
+
const clear = scope?.[clearName];
|
|
63
|
+
if (typeof original !== "function" || typeof clear !== "function") continue;
|
|
64
|
+
if (globals !== undefined && original === globals[name]) continue;
|
|
65
|
+
const owned = function (callback, ...rest) {
|
|
66
|
+
const cell = currentCell();
|
|
67
|
+
if (cell === undefined || typeof callback !== "function") return original.call(this, callback, ...rest);
|
|
68
|
+
if (cell.released) {
|
|
69
|
+
const never = original.call(this, () => {}, ...rest);
|
|
70
|
+
clear.call(scope, never);
|
|
71
|
+
return never;
|
|
72
|
+
}
|
|
73
|
+
let forget = () => {};
|
|
74
|
+
const handle = original.call(
|
|
75
|
+
this,
|
|
76
|
+
function (...args) {
|
|
77
|
+
if (name !== "setInterval") forget();
|
|
78
|
+
return callback.apply(this, args);
|
|
79
|
+
},
|
|
80
|
+
...rest,
|
|
81
|
+
);
|
|
82
|
+
forget = onReleaseOf(cell, () => clear.call(scope, handle));
|
|
83
|
+
return handle;
|
|
84
|
+
};
|
|
85
|
+
Object.assign(owned, original);
|
|
86
|
+
replace(scope, name, owned, patched);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function patchPromiseApi(owner, name, patched) {
|
|
91
|
+
const original = owner?.[name];
|
|
92
|
+
if (typeof original !== "function") return;
|
|
93
|
+
const bound = function (...args) {
|
|
94
|
+
const cell = currentCell();
|
|
95
|
+
if (cell === undefined) return original.apply(this, args);
|
|
96
|
+
if (cell.released) return Promise.reject(cell.interruption);
|
|
97
|
+
return settleWithCell(cell, original.apply(this, args));
|
|
98
|
+
};
|
|
99
|
+
Object.assign(bound, original);
|
|
100
|
+
replace(owner, name, bound, patched);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function patchTimerPromises(patched) {
|
|
104
|
+
const timers = builtin("node:timers/promises");
|
|
105
|
+
if (timers === undefined) return;
|
|
106
|
+
patchPromiseApi(timers, "setTimeout", patched);
|
|
107
|
+
patchPromiseApi(timers, "setImmediate", patched);
|
|
108
|
+
const original = timers.setInterval;
|
|
109
|
+
if (typeof original !== "function") return;
|
|
110
|
+
const owned = function (...args) {
|
|
111
|
+
const cell = currentCell();
|
|
112
|
+
if (cell?.released) throw cell.interruption;
|
|
113
|
+
const iterator = original.apply(this, args);
|
|
114
|
+
if (cell === undefined) return iterator;
|
|
115
|
+
onReleaseOf(cell, () => void iterator.return?.());
|
|
116
|
+
return iterator;
|
|
117
|
+
};
|
|
118
|
+
replace(timers, "setInterval", owned, patched);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function patchFetch(scope, patched, name = "fetch") {
|
|
122
|
+
const original = scope[name];
|
|
123
|
+
if (typeof original !== "function") return;
|
|
124
|
+
const owned = function (input, init) {
|
|
125
|
+
const cell = currentCell();
|
|
126
|
+
if (cell === undefined) return original.call(this, input, init);
|
|
127
|
+
if (cell.released) return Promise.reject(cell.interruption);
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const own = init?.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
130
|
+
const signal = own === undefined ? controller.signal : AbortSignal.any([own, controller.signal]);
|
|
131
|
+
const forget = onReleaseOf(cell, () => controller.abort(cell.interruption));
|
|
132
|
+
return original.call(this, input, { ...init, signal }).finally(forget);
|
|
133
|
+
};
|
|
134
|
+
Object.assign(owned, original);
|
|
135
|
+
replace(scope, name, owned, patched);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A released cell's code may still be resumed by a short I/O completion; refusing new I/O at that point makes such a
|
|
139
|
+
// loop throw the interruption on its next operation instead of running on in the kept worker.
|
|
140
|
+
function refuseWhenReleased(owner, names, patched) {
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
const original = owner?.[name];
|
|
143
|
+
if (typeof original !== "function") continue;
|
|
144
|
+
const guarded = function (...args) {
|
|
145
|
+
const cell = currentCell();
|
|
146
|
+
if (cell?.released) throw cell.interruption;
|
|
147
|
+
const result = original.apply(this, args);
|
|
148
|
+
return cell === undefined ? result : settleWithCell(cell, result);
|
|
149
|
+
};
|
|
150
|
+
Object.assign(guarded, original);
|
|
151
|
+
replace(owner, name, guarded, patched);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The callback form: a released cell's operation is never started, so its callback never runs, the same way a timer a
|
|
156
|
+
// released cell sets never fires.
|
|
157
|
+
function neverStartWhenReleased(owner, names, patched) {
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
const original = owner?.[name];
|
|
160
|
+
if (typeof original !== "function") continue;
|
|
161
|
+
const guarded = function (...args) {
|
|
162
|
+
if (currentCell()?.released) return undefined;
|
|
163
|
+
return original.apply(this, args);
|
|
164
|
+
};
|
|
165
|
+
Object.assign(guarded, original);
|
|
166
|
+
replace(owner, name, guarded, patched);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const FS_OPERATIONS = ["readFile", "writeFile", "appendFile", "readdir", "stat", "lstat", "open", "access", "mkdir", "rm", "unlink", "rename", "copyFile"];
|
|
171
|
+
|
|
172
|
+
// For ES classes only (Worker, WebSocket, MessageChannel): `instanceof` keeps accepting instances the runtime creates
|
|
173
|
+
// internally from the original class. Node's function-style constructors are owned on activation instead.
|
|
174
|
+
function patchClass(scope, name, close, patched) {
|
|
175
|
+
const Original = scope?.[name];
|
|
176
|
+
if (typeof Original !== "function") return;
|
|
177
|
+
const Owned = class extends Original {
|
|
178
|
+
constructor(...args) {
|
|
179
|
+
const cell = currentCell();
|
|
180
|
+
if (cell?.released) throw cell.interruption;
|
|
181
|
+
super(...args);
|
|
182
|
+
if (cell !== undefined) onReleaseOf(cell, () => close(this));
|
|
183
|
+
}
|
|
184
|
+
static [Symbol.hasInstance](value) {
|
|
185
|
+
return value instanceof Original;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
Object.defineProperty(Owned, "name", { value: Original.name });
|
|
189
|
+
replace(scope, name, Owned, patched);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const NODE_RESOURCE_FACTORIES = {
|
|
193
|
+
"node:net": ["createConnection", "connect", "createServer"],
|
|
194
|
+
"node:tls": ["connect", "createServer"],
|
|
195
|
+
"node:http": ["request", "get", "createServer"],
|
|
196
|
+
"node:https": ["request", "get", "createServer"],
|
|
197
|
+
"node:dgram": ["createSocket"],
|
|
198
|
+
"node:fs": ["createReadStream", "createWriteStream", "watch"],
|
|
199
|
+
"node:readline": ["createInterface"],
|
|
200
|
+
"node:readline/promises": ["createInterface"],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Node's socket and server types are function-style constructors that Node's own subclasses call with `.call(this)`
|
|
204
|
+
// (http.Server calls net.Server), so they cannot be swapped for a class. A socket or server only does anything once it
|
|
205
|
+
// connects, listens or binds, so that call is where a cell takes ownership of it; http, https and tls inherit these.
|
|
206
|
+
const NODE_ACTIVATIONS = {
|
|
207
|
+
"node:net": { Socket: ["connect"], Server: ["listen"] },
|
|
208
|
+
"node:dgram": { Socket: ["bind"] },
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
function ownOnActivation(proto, names, patched) {
|
|
212
|
+
for (const name of names) {
|
|
213
|
+
const original = proto?.[name];
|
|
214
|
+
if (typeof original !== "function") continue;
|
|
215
|
+
const owned = new WeakSet();
|
|
216
|
+
const activate = function (...args) {
|
|
217
|
+
const cell = currentCell();
|
|
218
|
+
if (cell === undefined) return original.apply(this, args);
|
|
219
|
+
if (cell.released) throw cell.interruption;
|
|
220
|
+
if (!owned.has(this)) {
|
|
221
|
+
owned.add(this);
|
|
222
|
+
onReleaseOf(cell, () => closeResource(this));
|
|
223
|
+
}
|
|
224
|
+
return original.apply(this, args);
|
|
225
|
+
};
|
|
226
|
+
replace(proto, name, activate, patched);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function installCellOwnership(scope = globalThis) {
|
|
231
|
+
const patched = [];
|
|
232
|
+
patchTimers(scope, patched);
|
|
233
|
+
patchTimers(builtin("node:timers"), patched, scope);
|
|
234
|
+
patchTimerPromises(patched);
|
|
235
|
+
patchFetch(scope, patched);
|
|
236
|
+
patchClass(scope, "WebSocket", (socket) => socket.close(), patched);
|
|
237
|
+
patchClass(scope, "Worker", (worker) => void worker.terminate(), patched);
|
|
238
|
+
refuseWhenReleased(builtin("node:fs/promises"), FS_OPERATIONS, patched);
|
|
239
|
+
neverStartWhenReleased(builtin("node:fs"), FS_OPERATIONS, patched);
|
|
240
|
+
patchClass(scope, "MessageChannel", (channel) => {
|
|
241
|
+
channel.port1.close();
|
|
242
|
+
channel.port2.close();
|
|
243
|
+
}, patched);
|
|
244
|
+
patchClass(scope, "BroadcastChannel", (channel) => channel.close(), patched);
|
|
245
|
+
const bun = scope.Bun;
|
|
246
|
+
if (bun !== undefined) {
|
|
247
|
+
patchPromiseApi(bun, "sleep", patched);
|
|
248
|
+
if (bun.fetch !== scope.fetch) patchFetch(bun, patched, "fetch");
|
|
249
|
+
refuseWhenReleased(bun, ["file", "write"], patched);
|
|
250
|
+
ownResultsOf(bun, ["connect", "listen", "serve", "udpSocket"], patched);
|
|
251
|
+
}
|
|
252
|
+
for (const [module, factories] of Object.entries(NODE_RESOURCE_FACTORIES)) ownResultsOf(builtin(module), factories, patched);
|
|
253
|
+
patchClass(builtin("node:worker_threads"), "Worker", (worker) => void worker.terminate(), patched);
|
|
254
|
+
for (const [module, types] of Object.entries(NODE_ACTIVATIONS)) {
|
|
255
|
+
for (const [name, methods] of Object.entries(types)) ownOnActivation(builtin(module)?.[name]?.prototype, methods, patched);
|
|
256
|
+
}
|
|
257
|
+
// Node hands `import("node:…")` its own ESM namespace; without this sync a cell importing the module gets the
|
|
258
|
+
// unowned originals.
|
|
259
|
+
syncBuiltinExports();
|
|
260
|
+
return () => {
|
|
261
|
+
for (const undo of patched.reverse()) undo();
|
|
262
|
+
syncBuiltinExports();
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function syncBuiltinExports() {
|
|
267
|
+
builtin("node:module")?.syncBuiltinESMExports?.();
|
|
268
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
// Every continuation a cell creates (awaits, timers, I/O callbacks) runs inside its cell record. Stopping a cell
|
|
4
|
+
// releases it: everything it owns is cleared, rejected or closed (`cell-ownership.js` decides what a cell owns), and
|
|
5
|
+
// anything its code tries to start afterwards is refused, while the kernel and its variables stay.
|
|
6
|
+
const cellRuns = new AsyncLocalStorage();
|
|
7
|
+
const releasedInterruptions = new WeakMap();
|
|
8
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
9
|
+
|
|
10
|
+
export function runInCell(cell, run) {
|
|
11
|
+
return cellRuns.run(cell, run);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function currentCell() {
|
|
15
|
+
return cellRuns.getStore();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function releasedCellError() {
|
|
19
|
+
const cell = cellRuns.getStore();
|
|
20
|
+
return cell?.released === true ? cell.interruption : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertCellLive() {
|
|
24
|
+
const error = releasedCellError();
|
|
25
|
+
if (error !== undefined) throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function onCellRelease(close) {
|
|
29
|
+
const cell = cellRuns.getStore();
|
|
30
|
+
return cell === undefined ? () => {} : onReleaseOf(cell, close);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function onReleaseOf(cell, close) {
|
|
34
|
+
cell.onRelease ??= new Set();
|
|
35
|
+
cell.onRelease.add(close);
|
|
36
|
+
return () => cell.onRelease.delete(close);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Releases the cell: every closer runs even when an earlier one throws, and the failures are returned so the caller
|
|
41
|
+
* can report them; a resource that would not close is a fact the user should see, not something to swallow.
|
|
42
|
+
*/
|
|
43
|
+
export function releaseCell(cell) {
|
|
44
|
+
cell.released = true;
|
|
45
|
+
releasedInterruptions.set(cell.interruption, cell);
|
|
46
|
+
const failures = [];
|
|
47
|
+
for (const close of cell.onRelease ?? []) {
|
|
48
|
+
try {
|
|
49
|
+
close();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
failures.push(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
cell.onRelease?.clear();
|
|
55
|
+
return failures;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The cell an unhandled rejection came from: a released cell whose interruption is the reason or sits in its `cause`
|
|
60
|
+
* chain, or the cell whose async context the rejection runs in (Node exposes it to the handler; Bun does not).
|
|
61
|
+
*/
|
|
62
|
+
export function cellOfRejection(reason) {
|
|
63
|
+
let current = reason;
|
|
64
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof current === "object" && current !== null; depth++) {
|
|
65
|
+
const cell = releasedInterruptions.get(current);
|
|
66
|
+
if (cell !== undefined) return cell;
|
|
67
|
+
current = current.cause;
|
|
68
|
+
}
|
|
69
|
+
return cellRuns.getStore();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// An operation already in flight when its cell is released rejects with the interruption at once, so the stopped
|
|
73
|
+
// cell's continuation never runs again; a non-promise result is returned as it is.
|
|
74
|
+
export function settleWithCell(cell, result) {
|
|
75
|
+
if (result === null || typeof result !== "object" || typeof result.then !== "function") return result;
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const forget = onReleaseOf(cell, () => reject(cell.interruption));
|
|
78
|
+
result.then(
|
|
79
|
+
(value) => {
|
|
80
|
+
forget();
|
|
81
|
+
resolve(value);
|
|
82
|
+
},
|
|
83
|
+
(error) => {
|
|
84
|
+
forget();
|
|
85
|
+
reject(error);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
-
import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
3
2
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
-
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
5
3
|
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
6
4
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
7
|
-
import {
|
|
5
|
+
import { dispatchCell } from "./cell-dispatch.ts";
|
|
6
|
+
import { consumeControlFrame } from "./control-frames.ts";
|
|
7
|
+
import { HostEntries } from "./host-entries.ts";
|
|
8
|
+
import { DEFAULT_INTERRUPT_BOUNDS, JS_INTERRUPT_GRACE_MS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
8
9
|
import {
|
|
9
10
|
assertJavaScriptKernelOpen,
|
|
10
11
|
type JavaScriptKernelMode,
|
|
11
12
|
type JavaScriptRunInput,
|
|
12
13
|
type LifecycleState,
|
|
13
14
|
type ResultMessage,
|
|
14
|
-
resolveKernelToolNameSource,
|
|
15
15
|
type ToolCallMessage,
|
|
16
16
|
} from "./kernel-contract.ts";
|
|
17
|
-
import {
|
|
17
|
+
import { JavaScriptKernelMemory } from "./kernel-memory.ts";
|
|
18
|
+
import type { JavaScriptMemoryReading } from "./kernel-memory-bridge.ts";
|
|
18
19
|
import { kernelToolError } from "./kernel-tools-errors.ts";
|
|
19
20
|
import type {
|
|
20
21
|
KernelToolsDescribeResult,
|
|
@@ -22,10 +23,9 @@ import type {
|
|
|
22
23
|
KernelToolsInvokeRequest,
|
|
23
24
|
} from "./kernel-tools-types.ts";
|
|
24
25
|
import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
|
|
25
|
-
import { JavaScriptRunQueue } from "./run-queue.ts";
|
|
26
|
+
import { JavaScriptRunQueue, type PendingJavaScriptRun } from "./run-queue.ts";
|
|
26
27
|
import { ToolCallQueue } from "./tool-call-queue.ts";
|
|
27
28
|
import { WorkerChildren } from "./worker-children.ts";
|
|
28
|
-
import { crashedResult } from "./worker-host.ts";
|
|
29
29
|
import { WorkerRecovery } from "./worker-recovery.ts";
|
|
30
30
|
import { WorkerSlot } from "./worker-slot.ts";
|
|
31
31
|
|
|
@@ -35,6 +35,15 @@ export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
|
35
35
|
export { type JavaScriptWorkerEntryUrlOptions, resolveJsWorkerEntryUrl } from "./worker-startup.ts";
|
|
36
36
|
|
|
37
37
|
export class JavaScriptKernel {
|
|
38
|
+
readonly #hostEntries = new HostEntries<PendingJavaScriptRun>({
|
|
39
|
+
emit: (run, message) => (run.input.onMessage ?? this.#options.onMessage)?.(message),
|
|
40
|
+
settle: (run, result) => {
|
|
41
|
+
if (!this.#runs.releaseActive(run)) return;
|
|
42
|
+
this.#runs.settle(run, result);
|
|
43
|
+
this.#startNext();
|
|
44
|
+
},
|
|
45
|
+
durationMs: (run) => this.#runs.durationMs(run, performance.now()),
|
|
46
|
+
});
|
|
38
47
|
readonly #options: JavaScriptKernelOptions;
|
|
39
48
|
readonly #moduleLoader: LocalModuleLoader;
|
|
40
49
|
readonly #slot: WorkerSlot;
|
|
@@ -48,7 +57,7 @@ export class JavaScriptKernel {
|
|
|
48
57
|
);
|
|
49
58
|
readonly #toolCalls = new ToolCallQueue();
|
|
50
59
|
readonly #children: WorkerChildren;
|
|
51
|
-
readonly #memory:
|
|
60
|
+
readonly #memory: JavaScriptKernelMemory;
|
|
52
61
|
readonly #activeCell: ActiveCellControl;
|
|
53
62
|
readonly #recovery = new WorkerRecovery({
|
|
54
63
|
runs: this.#runs,
|
|
@@ -67,7 +76,7 @@ export class JavaScriptKernel {
|
|
|
67
76
|
recover: () => void this.#recovery.recover(() => Promise.resolve()),
|
|
68
77
|
clearToolCalls: () => this.#toolCalls.clear(),
|
|
69
78
|
});
|
|
70
|
-
this.#memory = new
|
|
79
|
+
this.#memory = new JavaScriptKernelMemory(options, () => this.#slot.processPid);
|
|
71
80
|
this.#children = new WorkerChildren(options.collectOrphanedChildren);
|
|
72
81
|
this.#moduleLoader = new LocalModuleLoader(options);
|
|
73
82
|
this.#slot = new WorkerSlot(options, {
|
|
@@ -81,15 +90,20 @@ export class JavaScriptKernel {
|
|
|
81
90
|
return this.#slot.mode;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
/** The
|
|
93
|
+
/** The kernel process's pid in process mode; none in worker mode or before it starts. */
|
|
94
|
+
get processPid(): number | undefined {
|
|
95
|
+
return this.#slot.processPid;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The last memory reading (a result, an idle collection, a query); none before the first. */
|
|
85
99
|
get lastLiveBytes(): number | undefined {
|
|
86
100
|
return this.#memory.lastLiveBytes;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
/** A heap reading taken between cells without running one; nothing when no worker is live to ask. */
|
|
90
104
|
async queryMemory(): Promise<JavaScriptMemoryReading | undefined> {
|
|
91
|
-
|
|
92
|
-
return await this.#memory.query((message) => this.#slot.postMessage(message));
|
|
105
|
+
const ready = this.#lifecycle === "open" && this.#slot.present && !this.#slot.startingUp;
|
|
106
|
+
return await this.#memory.query(ready ? (message) => this.#slot.postMessage(message) : undefined);
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
get kernelToolEvents(): EventTarget {
|
|
@@ -129,6 +143,7 @@ export class JavaScriptKernel {
|
|
|
129
143
|
const cancelled = this.cancelQueued(cellId, reason);
|
|
130
144
|
return { stateRetained: Promise.resolve(true), ...(cancelled ? {} : { note: "cell not found" }) };
|
|
131
145
|
}
|
|
146
|
+
if (active && this.#hostEntries.abort(active, reason)) return { stateRetained: Promise.resolve(true) };
|
|
132
147
|
if (!active) {
|
|
133
148
|
// A worker still stuck in startup is not a healthy idle worker: retiring it is the only recovery.
|
|
134
149
|
const wedgedInStartup = this.#slot.startingUp;
|
|
@@ -163,11 +178,14 @@ export class JavaScriptKernel {
|
|
|
163
178
|
if (this.#closePromise) return await this.#closePromise;
|
|
164
179
|
this.#slot.postMessage({ type: "close" });
|
|
165
180
|
this.#lifecycle = "closing";
|
|
181
|
+
const graceMs = this.#options.interruptBounds?.graceMs ?? JS_INTERRUPT_GRACE_MS;
|
|
182
|
+
const hostEntriesStopped = this.#hostEntries.stopAll("JS kernel closed", graceMs);
|
|
166
183
|
this.#runs.settleAll("JS kernel closed");
|
|
167
184
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", "JS kernel closed"));
|
|
168
185
|
this.#toolCalls.clear();
|
|
169
186
|
const recovery = this.#recovery.inFlight;
|
|
170
187
|
const closePromise = (async () => {
|
|
188
|
+
await hostEntriesStopped;
|
|
171
189
|
if (recovery) await recovery;
|
|
172
190
|
await this.#terminate();
|
|
173
191
|
})().finally(() => {
|
|
@@ -189,38 +207,26 @@ export class JavaScriptKernel {
|
|
|
189
207
|
|
|
190
208
|
async #ensureReady(): Promise<void> {
|
|
191
209
|
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
192
|
-
|
|
210
|
+
try {
|
|
211
|
+
await this.#slot.ensureReady();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (this.#options.isolation !== "process") throw error;
|
|
214
|
+
// Process mode reports a failed start on the waiting cell, never as a rejection of run().
|
|
215
|
+
if (this.#lifecycle === "open") this.#runs.settleAll(error instanceof Error ? error.message : String(error));
|
|
216
|
+
}
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
#startNext(): void {
|
|
196
220
|
if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
|
|
197
221
|
const next = this.#runs.startNext(performance.now());
|
|
198
222
|
if (!next) return;
|
|
199
|
-
const
|
|
200
|
-
if ("
|
|
201
|
-
this.#
|
|
202
|
-
this.#runs.settle(next, {
|
|
203
|
-
type: "result",
|
|
204
|
-
cellId: next.input.cellId,
|
|
205
|
-
ok: false,
|
|
206
|
-
error: { message: input.refused },
|
|
207
|
-
durationMs: 0,
|
|
208
|
-
});
|
|
209
|
-
this.#startNext();
|
|
223
|
+
const dispatch = dispatchCell(next.input, this.#moduleLoader, this.#options);
|
|
224
|
+
if (dispatch.kind === "host") {
|
|
225
|
+
this.#hostEntries.start(next, dispatch.host);
|
|
210
226
|
return;
|
|
211
227
|
}
|
|
212
228
|
this.#activeCell.arm(next);
|
|
213
|
-
this.#slot.postMessage(
|
|
214
|
-
type: "kernel-tools-names",
|
|
215
|
-
hostToolNames: resolveKernelToolNameSource(this.#options.hostToolNames),
|
|
216
|
-
foreignLanguageNames: resolveKernelToolNameSource(this.#options.foreignLanguageNames),
|
|
217
|
-
});
|
|
218
|
-
this.#slot.postMessage({
|
|
219
|
-
type: "run",
|
|
220
|
-
cellId: next.input.cellId,
|
|
221
|
-
code: this.#moduleLoader.prepareCell(input.code, input.kernelPreludes, input.sourceFile),
|
|
222
|
-
timeoutMs: next.input.timeoutMs,
|
|
223
|
-
});
|
|
229
|
+
for (const frame of dispatch.frames) this.#slot.postMessage(frame);
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
async #restartAfterStop(): Promise<void> {
|
|
@@ -229,15 +235,7 @@ export class JavaScriptKernel {
|
|
|
229
235
|
|
|
230
236
|
#handleMessage(message: KernelToHostMessage): void {
|
|
231
237
|
if (this.#kernelTools.consume(message) && message.type !== "tool-call") return;
|
|
232
|
-
if (this.#memory
|
|
233
|
-
if (message.type === "status" && message.event.op === INTERRUPT_ACK_OP) {
|
|
234
|
-
this.#runs.acknowledgeInterrupt(message.event);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (message.type === "status" && message.event.op === CHILD_LIFECYCLE_OP) {
|
|
238
|
-
this.#children.track(message.event);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
238
|
+
if (consumeControlFrame(message, { memory: this.#memory, runs: this.#runs, children: this.#children })) return;
|
|
241
239
|
(this.#runs.active?.input.onMessage ?? this.#options.onMessage)?.(message);
|
|
242
240
|
if (message.type === "tool-call") {
|
|
243
241
|
this.#toolCalls.push(message);
|
|
@@ -260,13 +258,10 @@ export class JavaScriptKernel {
|
|
|
260
258
|
if (!active && this.#slot.startingUp) return;
|
|
261
259
|
this.#activeCell.disarm();
|
|
262
260
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", error.message));
|
|
263
|
-
this.#memory.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.#runs.
|
|
267
|
-
active,
|
|
268
|
-
crashedResult(active.input.cellId, error, this.#runs.durationMs(active, performance.now())),
|
|
269
|
-
);
|
|
261
|
+
this.#memory.crashed(error);
|
|
262
|
+
// A crash mid-install stops the install (process group killed, nothing published); it settles itself once stopped.
|
|
263
|
+
if (active && !this.#hostEntries.abort(active, `JavaScript worker crashed: ${error.message}`)) {
|
|
264
|
+
this.#runs.settleCrashed(active, error);
|
|
270
265
|
}
|
|
271
266
|
this.#toolCalls.clear();
|
|
272
267
|
void this.#restartAfterStop();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
3
|
+
import type { JavaScriptKernelMemory } from "./kernel-memory.ts";
|
|
4
|
+
import type { JavaScriptRunQueue } from "./run-queue.ts";
|
|
5
|
+
import type { WorkerChildren } from "./worker-children.ts";
|
|
6
|
+
|
|
7
|
+
export interface ControlFrameOwners {
|
|
8
|
+
readonly memory: Pick<JavaScriptKernelMemory, "consume">;
|
|
9
|
+
readonly runs: Pick<JavaScriptRunQueue, "acknowledgeInterrupt">;
|
|
10
|
+
readonly children: Pick<WorkerChildren, "track">;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Routes a worker's side-channel frames (memory readings, interrupt acks, child lifecycle) to their owners. */
|
|
14
|
+
export function consumeControlFrame(message: KernelToHostMessage, owners: ControlFrameOwners): boolean {
|
|
15
|
+
if (owners.memory.consume(message)) return true;
|
|
16
|
+
if (message.type !== "status") return false;
|
|
17
|
+
if (message.event.op === INTERRUPT_ACK_OP) {
|
|
18
|
+
owners.runs.acknowledgeInterrupt(message.event);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (message.event.op !== CHILD_LIFECYCLE_OP) return false;
|
|
22
|
+
owners.children.track(message.event);
|
|
23
|
+
return true;
|
|
24
|
+
}
|