@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,52 @@
|
|
|
1
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
2
|
+
|
|
3
|
+
// `require` in a cell resolves the way `import` does there: builtins natively, relative paths from the cell's
|
|
4
|
+
// directory, bare packages from the session's project first and then its managed package environment
|
|
5
|
+
// (%bun add / %npm add). It uses the runtime's own CommonJS loader, so JSON files and the `require` export conditions
|
|
6
|
+
// behave as in Node. Like Node's own `require`, it carries `resolve`, `resolve.paths` and `cache`.
|
|
7
|
+
export function createCellRequire(context) {
|
|
8
|
+
const requirers = () => {
|
|
9
|
+
const { cwdUrl, packageRootUrl } = context();
|
|
10
|
+
const fromCwd = createRequire(new URL("package.json", cwdUrl));
|
|
11
|
+
const fromPackages = packageRootUrl ? createRequire(new URL("package.json", packageRootUrl)) : undefined;
|
|
12
|
+
return { fromCwd, fromPackages };
|
|
13
|
+
};
|
|
14
|
+
const isPathLike = (name) => name.startsWith(".") || name.startsWith("/") || /^[A-Za-z]:[\\/]/.test(name);
|
|
15
|
+
// The one lookup both the call and `resolve` use, so they can never pick different copies of a package. Builtins
|
|
16
|
+
// (`fs`, `node:fs`, Bun's `bun:sqlite`) and paths resolve natively; a bare name the project does not have falls
|
|
17
|
+
// back to the managed environment, and a miss there reports the project's own not-found error.
|
|
18
|
+
const resolve = function resolve(specifier, options) {
|
|
19
|
+
const name = String(specifier);
|
|
20
|
+
const { fromCwd, fromPackages } = requirers();
|
|
21
|
+
if (isBuiltin(name) || isPathLike(name) || fromPackages === undefined || options?.paths !== undefined) {
|
|
22
|
+
return fromCwd.resolve(name, options);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return fromCwd.resolve(name, options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error?.code !== "MODULE_NOT_FOUND") throw error;
|
|
28
|
+
try {
|
|
29
|
+
return fromPackages.resolve(name, options);
|
|
30
|
+
} catch (fallback) {
|
|
31
|
+
throw fallback?.code === "MODULE_NOT_FOUND" ? error : fallback;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
resolve.paths = (specifier) => {
|
|
36
|
+
const name = String(specifier);
|
|
37
|
+
const { fromCwd, fromPackages } = requirers();
|
|
38
|
+
const own = fromCwd.resolve.paths(name);
|
|
39
|
+
if (own === null || fromPackages === undefined || isPathLike(name)) return own;
|
|
40
|
+
return [...new Set([...own, ...(fromPackages.resolve.paths(name) ?? [])])];
|
|
41
|
+
};
|
|
42
|
+
// The call loads exactly what `resolve` names: a builtin id or an absolute file, through the runtime's loader.
|
|
43
|
+
const require = function require(specifier) {
|
|
44
|
+
return requirers().fromCwd(resolve(specifier));
|
|
45
|
+
};
|
|
46
|
+
require.resolve = resolve;
|
|
47
|
+
// One CommonJS cache per runtime: the cell's view is the loader's own, so deleting an entry forces a reload.
|
|
48
|
+
Object.defineProperty(require, "cache", { enumerable: true, get: () => createRequire(import.meta.url).cache });
|
|
49
|
+
return require;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { createRequire };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// allow: SIZE_OK — private runtime state and installed globals must stay in one worker module.
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createCellRequire, createRequire } from "./worker-require.js";
|
|
2
4
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
5
|
import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
4
6
|
import { inspect } from "node:util";
|
|
5
7
|
import { encodeDisplayImage, resolveDisplayOps } from "./display-image.js";
|
|
6
|
-
import { terminateProcessTrees } from "./process-tree.js";
|
|
8
|
+
import { terminateProcessGroups, terminateProcessTrees } from "./process-tree.js";
|
|
7
9
|
import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect-eval.js";
|
|
10
|
+
import { INJECTED_GROUP } from "./worker-cwd.js";
|
|
8
11
|
import { installShellCapture } from "./worker-shell-capture.js";
|
|
9
12
|
import { bindKernelBun } from "./worker-webview.js";
|
|
10
13
|
import { createWorkpool } from "./workpool.js";
|
|
@@ -31,6 +34,10 @@ export class JsWorkerRuntime {
|
|
|
31
34
|
#hooks = null;
|
|
32
35
|
#pendingDisplays = [];
|
|
33
36
|
#children = new Set();
|
|
37
|
+
// Groups the worker created for cell children (senpi#2995). A group outlives its leader, so a grandchild re-parented
|
|
38
|
+
// to init is still found through it at retirement (senpi#3020).
|
|
39
|
+
#ownedGroups = new Map();
|
|
40
|
+
#childrenStopping;
|
|
34
41
|
#shellWaits = new Set();
|
|
35
42
|
#onChildEvent;
|
|
36
43
|
#onShellWaitChange;
|
|
@@ -47,6 +54,7 @@ export class JsWorkerRuntime {
|
|
|
47
54
|
generation: options.kernelGeneration ?? 1,
|
|
48
55
|
hostToolNames: options.hostToolNames ?? [],
|
|
49
56
|
foreignLanguageNames: options.foreignLanguageNames ?? [],
|
|
57
|
+
disabled: options.kernelToolsDisabled === true,
|
|
50
58
|
});
|
|
51
59
|
this.#installGlobals();
|
|
52
60
|
}
|
|
@@ -73,32 +81,48 @@ export class JsWorkerRuntime {
|
|
|
73
81
|
if (typeof prepared.sourceFile === "string") sourceName = prepared.sourceFile;
|
|
74
82
|
}
|
|
75
83
|
if (prelude) indirectEval(prelude, PRELUDE_SOURCE_URL);
|
|
76
|
-
const
|
|
84
|
+
const shadowed = [];
|
|
85
|
+
const source = bindKernelBun(wrapUserCode(cellCode, shadowed));
|
|
86
|
+
if (shadowed.length > 0) this.#emitText("stderr", shadowNote(shadowed));
|
|
87
|
+
const value = await awaitMaybePromise(indirectEval(source, sourceName));
|
|
77
88
|
await this.#drainPendingDisplays();
|
|
78
89
|
return value;
|
|
79
90
|
} finally {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// spawned it is over, nothing will await it again, and it would be
|
|
83
|
-
// reparented to init. Retire it the way timeout and abort cleanup
|
|
84
|
-
// already do, unless the cell asked for a detached process.
|
|
85
|
-
await this.#terminateChildren();
|
|
86
|
-
this.#hooks = null;
|
|
91
|
+
// A released run finishing late must not clear the state of the cell running now.
|
|
92
|
+
if (this.#hooks === hooks) await this.release();
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/** Ends the current run's ownership now: its displays, its hooks, and (unless detached) its child processes. */
|
|
97
|
+
async release() {
|
|
98
|
+
const hooks = this.#hooks;
|
|
99
|
+
this.#pendingDisplays = [];
|
|
100
|
+
// A child still running here has lost its only owner: the cell that
|
|
101
|
+
// spawned it is over, nothing will await it again, and it would be
|
|
102
|
+
// reparented to init. Retire it the way timeout and abort cleanup
|
|
103
|
+
// already do, unless the cell asked for a detached process.
|
|
104
|
+
await Promise.all([this.#childrenStopping, this.#terminateChildren()]);
|
|
105
|
+
this.#childrenStopping = undefined;
|
|
106
|
+
if (this.#hooks === hooks) this.#hooks = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
interrupt() {
|
|
91
110
|
// The tree snapshot, SIGTERM, and SIGKILL escalation run on their own so
|
|
92
111
|
// the caller's interrupt latency stays that of the acknowledgement.
|
|
93
|
-
|
|
112
|
+
this.#childrenStopping = this.#terminateChildren();
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
#trackChild(child, spawnOptions) {
|
|
97
116
|
if (child === null || typeof child !== "object" || typeof child.kill !== "function") return;
|
|
98
117
|
// `detached: true` is the cell saying it wants the process to outlive it.
|
|
99
|
-
|
|
118
|
+
const groupInjected = (globalThis[INJECTED_GROUP] ?? 0) > 0;
|
|
119
|
+
if (isPlainObject(spawnOptions) && spawnOptions.detached === true && !groupInjected) return;
|
|
100
120
|
this.#children.add(child);
|
|
101
121
|
const pid = Number.isInteger(child.pid) && child.pid > 0 ? child.pid : null;
|
|
122
|
+
// Bun.spawn children get their group from worker-cwd.js without the injection marker, so every tracked child is
|
|
123
|
+
// recorded; a child that is not a group leader names no group, and signalling it later finds nothing.
|
|
124
|
+
const ownGroupLeader = !(isPlainObject(spawnOptions) && spawnOptions.detached === false);
|
|
125
|
+
if (pid !== null && process.platform !== "win32" && ownGroupLeader) this.#ownedGroups.set(pid, child);
|
|
102
126
|
// The host keeps its own copy of live pids: if this worker is terminated
|
|
103
127
|
// while blocked, only the host can still retire them.
|
|
104
128
|
if (pid !== null) this.#onChildEvent?.({ pid, state: "spawned" });
|
|
@@ -112,10 +136,20 @@ export class JsWorkerRuntime {
|
|
|
112
136
|
#terminateChildren() {
|
|
113
137
|
const children = [...this.#children].filter(child => child.exitCode === null && child.signalCode === null);
|
|
114
138
|
this.#children.clear();
|
|
115
|
-
|
|
139
|
+
// A leader that already exited leaves its group id reserved only while the group has members; if that pid now
|
|
140
|
+
// names a live process, it was reused by someone else and the group is not ours to signal (senpi#3020 review).
|
|
141
|
+
const groups = [...this.#ownedGroups].map(([pgid, child]) => ({
|
|
142
|
+
pgid,
|
|
143
|
+
leaderExited: child.exitCode !== null || child.signalCode !== null,
|
|
144
|
+
}));
|
|
145
|
+
this.#ownedGroups.clear();
|
|
146
|
+
if (children.length === 0 && groups.length === 0) return undefined;
|
|
116
147
|
const roots = children.map(child => child.pid).filter(pid => Number.isInteger(pid) && pid > 0);
|
|
117
148
|
const settled = Promise.allSettled(children.map(child => (child.exited instanceof Promise ? child.exited : Promise.resolve())));
|
|
118
|
-
return
|
|
149
|
+
return Promise.all([
|
|
150
|
+
roots.length > 0 ? terminateProcessTrees(roots, { graceMs: CHILD_TERMINATION_GRACE_MS, settled }) : undefined,
|
|
151
|
+
terminateProcessGroups(groups, { graceMs: CHILD_TERMINATION_GRACE_MS }),
|
|
152
|
+
]);
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
async #drainPendingDisplays() {
|
|
@@ -147,6 +181,17 @@ export class JsWorkerRuntime {
|
|
|
147
181
|
return isPlainObject(opts) && opts.handle === true ? handles.handle(value) : value;
|
|
148
182
|
};
|
|
149
183
|
globalThis.wait = async (list, options) => await handles.wait(list, options);
|
|
184
|
+
// The `%bun add` / `%npm add` installer as a call: same environment, receipt, cancellation and codes (senpi row 38).
|
|
185
|
+
globalThis.packages = Object.freeze({
|
|
186
|
+
install: async (manager, requirements, options = {}) => {
|
|
187
|
+
if (!isPlainObject(options)) throw new TypeError("packages.install(): options must be an object, e.g. { timeout: 120 }");
|
|
188
|
+
return await this.#callTool("__packages_install__", {
|
|
189
|
+
manager,
|
|
190
|
+
requirements,
|
|
191
|
+
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
});
|
|
150
195
|
globalThis.handle = value => handles.handle(value);
|
|
151
196
|
globalThis.tool = createToolNamespace(
|
|
152
197
|
(fn, metadata) => this.#tools.define(fn, metadata),
|
|
@@ -154,6 +199,8 @@ export class JsWorkerRuntime {
|
|
|
154
199
|
{ defined: () => this.#tools.defined(), undefine: (name) => this.#tools.undefine(name) },
|
|
155
200
|
);
|
|
156
201
|
globalThis.tools = globalThis.tool;
|
|
202
|
+
globalThis.require = createCellRequire(() => globalThis.__senpi_module_context__ ?? { cwdUrl: pathToFileURL(`${process.cwd()}/`).href });
|
|
203
|
+
globalThis.createRequire = createRequire;
|
|
157
204
|
const originalLog = console.log.bind(console);
|
|
158
205
|
const originalError = console.error.bind(console);
|
|
159
206
|
const originalStdoutWrite = process.stdout.write;
|
|
@@ -422,6 +469,12 @@ export class JsWorkerRuntime {
|
|
|
422
469
|
}
|
|
423
470
|
}
|
|
424
471
|
|
|
472
|
+
function shadowNote(names) {
|
|
473
|
+
const list = names.map((name) => `\`${name}\``).join(", ");
|
|
474
|
+
const restore = names.map((name) => `delete ${name}`).join("; ");
|
|
475
|
+
return `Note: ${list} ${names.length === 1 ? "shadows a kernel or platform global" : "shadow kernel or platform globals"} in your later cells; the kernel and imported libraries keep the original. \`${restore}\` restores ${names.length === 1 ? "it" : "them"}.\n`;
|
|
476
|
+
}
|
|
477
|
+
|
|
425
478
|
function isPlainObject(value) {
|
|
426
479
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
427
480
|
}
|
|
@@ -12,10 +12,6 @@ function protectedGlobals() {
|
|
|
12
12
|
return protectedGlobalNames;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
for (const name of bindings) {
|
|
18
|
-
if (names.has(name)) return name;
|
|
19
|
-
}
|
|
20
|
-
return undefined;
|
|
15
|
+
export function isKernelGlobal(name) {
|
|
16
|
+
return protectedGlobals().has(name);
|
|
21
17
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { assertCellLive } from "./cell-run-context.js";
|
|
2
|
+
import { groupSignalNotice, noticeChildProcessGroupSignals, shellCommandText, signalsProcessGroup } from "./group-signal-notice.js";
|
|
3
|
+
|
|
1
4
|
const SHELL_CONFIG_METHODS = ["env", "cwd", "nothrow", "throws"];
|
|
2
5
|
const SHELL_READ_METHODS = ["text", "json", "lines", "arrayBuffer", "bytes", "blob"];
|
|
3
6
|
// `true | ( … )` hands every command in the template an empty pipe as stdin. The worker thread shares
|
|
@@ -26,7 +29,9 @@ export function installShellCapture(options) {
|
|
|
26
29
|
bun.$ = capturedShell(originalShell, options);
|
|
27
30
|
bun.spawn = capturedSpawn(originalSpawn, options, pinEnv);
|
|
28
31
|
if (originalSpawnSync !== null) bun.spawnSync = capturedSpawnSync(originalSpawnSync, pinEnv);
|
|
32
|
+
const restoreChildProcessNotice = noticeChildProcessGroupSignals(options.emitText, options.isActive);
|
|
29
33
|
return () => {
|
|
34
|
+
restoreChildProcessNotice();
|
|
30
35
|
bun.$ = originalShell;
|
|
31
36
|
bun.spawn = originalSpawn;
|
|
32
37
|
if (originalSpawnSync !== null) bun.spawnSync = originalSpawnSync;
|
|
@@ -39,7 +44,9 @@ function isBunRuntime(bun) {
|
|
|
39
44
|
|
|
40
45
|
function capturedShell(originalShell, options) {
|
|
41
46
|
const shell = (strings, ...expressions) => {
|
|
47
|
+
assertCellLive();
|
|
42
48
|
if (!options.isActive()) return originalShell(strings, ...expressions);
|
|
49
|
+
if (signalsProcessGroup(shellCommandText(strings, expressions))) options.emitText("stderr", groupSignalNotice("Bun.$"));
|
|
43
50
|
const promise = originalShell(isolateStdin(strings), ...expressions);
|
|
44
51
|
return captureShellPromise(promise, options);
|
|
45
52
|
};
|
|
@@ -132,6 +139,7 @@ function outputText(value) {
|
|
|
132
139
|
// without an explicit env must get the worker's view pinned too (measured on Bun 1.4.0).
|
|
133
140
|
function capturedSpawnSync(originalSpawnSync, pinEnv) {
|
|
134
141
|
return (...args) => {
|
|
142
|
+
assertCellLive();
|
|
135
143
|
if (!pinEnv) return originalSpawnSync(...args);
|
|
136
144
|
const [first, second] = args;
|
|
137
145
|
if (Array.isArray(first)) {
|
|
@@ -148,6 +156,7 @@ function capturedSpawnSync(originalSpawnSync, pinEnv) {
|
|
|
148
156
|
|
|
149
157
|
function capturedSpawn(originalSpawn, options, pinEnv) {
|
|
150
158
|
return (...args) => {
|
|
159
|
+
assertCellLive();
|
|
151
160
|
if (!options.isActive()) return originalSpawn(...args);
|
|
152
161
|
const [first, second] = args;
|
|
153
162
|
let child;
|
|
@@ -19,6 +19,7 @@ export class WorkerSlot {
|
|
|
19
19
|
readonly #options: JavaScriptKernelOptions;
|
|
20
20
|
readonly #listeners: WorkerSlotListeners;
|
|
21
21
|
#worker: WorkerLike | null = null;
|
|
22
|
+
#lastProcessPid: number | undefined;
|
|
22
23
|
#webViews: KernelWebViewClients | null = null;
|
|
23
24
|
#mode: JavaScriptKernelMode = "worker";
|
|
24
25
|
#generation = 0;
|
|
@@ -34,6 +35,12 @@ export class WorkerSlot {
|
|
|
34
35
|
return this.#mode;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
get processPid(): number | undefined {
|
|
39
|
+
const worker = this.#worker;
|
|
40
|
+
if (worker?.mode === "process") return worker.pid;
|
|
41
|
+
return this.#lastProcessPid;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
get present(): boolean {
|
|
38
45
|
return this.#worker !== null;
|
|
39
46
|
}
|
|
@@ -105,6 +112,7 @@ export class WorkerSlot {
|
|
|
105
112
|
#publish(worker: WorkerLike, generation: number): void {
|
|
106
113
|
if (!this.#listeners.isOpen() || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
107
114
|
this.#worker = worker;
|
|
115
|
+
if (worker.mode === "process") this.#lastProcessPid = worker.pid;
|
|
108
116
|
const webViews = new KernelWebViewClients((message, transfer) => worker.postMessage(message, transfer));
|
|
109
117
|
this.#webViews = webViews;
|
|
110
118
|
worker.onMessage((message) => {
|
|
@@ -2,8 +2,10 @@ import { dirname, join } from "node:path";
|
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
4
|
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
5
|
+
import { retireWorker } from "./interrupt-bounds.ts";
|
|
5
6
|
import { resolveKernelToolNameSource } from "./kernel-contract.ts";
|
|
6
7
|
import { type JavaScriptKernelOptions, localBridgeConnection } from "./local-module-loader.ts";
|
|
8
|
+
import { resolveJsProcessEntryUrl, spawnProcessWorker } from "./process-worker.ts";
|
|
7
9
|
import { spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
|
|
8
10
|
|
|
9
11
|
export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
|
|
@@ -15,6 +17,21 @@ export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions
|
|
|
15
17
|
return pathToFileURL(requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How long a process-mode kernel child gets to report ready. A child that a starved host never schedules would
|
|
22
|
+
* otherwise hold every queued cell forever: no cell's own timeout is armed until the kernel is up.
|
|
23
|
+
*/
|
|
24
|
+
export const PROCESS_STARTUP_DEADLINE_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
export class ProcessKernelStartupTimeoutError extends Error {
|
|
27
|
+
constructor(deadlineMs: number) {
|
|
28
|
+
super(
|
|
29
|
+
`JavaScript kernel process did not become ready within ${deadlineMs / 1000}s; it was stopped, and the next cell starts a fresh one`,
|
|
30
|
+
);
|
|
31
|
+
this.name = "ProcessKernelStartupTimeoutError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export interface WorkerStartupHooks {
|
|
19
36
|
readonly options: JavaScriptKernelOptions;
|
|
20
37
|
readonly kernelGeneration: number;
|
|
@@ -36,6 +53,13 @@ export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, s
|
|
|
36
53
|
await worker.terminate();
|
|
37
54
|
throw new WorkerStartupCancelledError();
|
|
38
55
|
}
|
|
56
|
+
// Process mode never falls back: the user asked for isolation, so a failed start stays visible. The child that
|
|
57
|
+
// failed to start is stopped (bounded), so a stuck one is not left behind.
|
|
58
|
+
if (worker.mode === "process") {
|
|
59
|
+
hooks.retire(worker);
|
|
60
|
+
await retireWorker(worker);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
39
63
|
if (worker.mode === "inline") throw error;
|
|
40
64
|
hooks.retire(worker);
|
|
41
65
|
await worker.terminate();
|
|
@@ -47,6 +71,15 @@ export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, s
|
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
|
|
74
|
+
if (options.isolation === "process") {
|
|
75
|
+
const url = resolveJsProcessEntryUrl();
|
|
76
|
+
return spawnProcessWorker(url, {
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
parallelPoolWidth: options.parallelPoolWidth,
|
|
79
|
+
...(options.processCommandPath === undefined ? {} : { searchPath: options.processCommandPath }),
|
|
80
|
+
...(options.processExecPath === undefined ? {} : { execPath: options.processExecPath }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
50
83
|
try {
|
|
51
84
|
const url = options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
|
|
52
85
|
return spawnNodeWorker(url, options.cwd, options.parallelPoolWidth);
|
|
@@ -57,7 +90,10 @@ function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
|
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
async function initializeWorker(worker: WorkerLike, hooks: WorkerStartupHooks, signal: AbortSignal): Promise<void> {
|
|
60
|
-
const ready =
|
|
93
|
+
const ready =
|
|
94
|
+
worker.mode === "process"
|
|
95
|
+
? withStartupDeadline(waitForReady(worker, signal), hooks.options)
|
|
96
|
+
: waitForReady(worker, signal);
|
|
61
97
|
const options = hooks.options;
|
|
62
98
|
worker.postMessage({
|
|
63
99
|
type: "init",
|
|
@@ -68,6 +104,18 @@ async function initializeWorker(worker: WorkerLike, hooks: WorkerStartupHooks, s
|
|
|
68
104
|
foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
|
|
69
105
|
...(options.sessionEnv === undefined ? {} : { sessionEnv: options.sessionEnv }),
|
|
70
106
|
...(options.memory === undefined ? {} : { memory: options.memory }),
|
|
107
|
+
...(options.kernelToolsEnabled === false ? { kernelToolsDisabled: true } : {}),
|
|
71
108
|
});
|
|
72
109
|
await ready;
|
|
73
110
|
}
|
|
111
|
+
|
|
112
|
+
function withStartupDeadline(ready: Promise<void>, options: JavaScriptKernelOptions): Promise<void> {
|
|
113
|
+
const deadlineMs = options.processStartupDeadlineMs ?? PROCESS_STARTUP_DEADLINE_MS;
|
|
114
|
+
let timer: NodeJS.Timeout | undefined;
|
|
115
|
+
const expired = new Promise<never>((_resolve, reject) => {
|
|
116
|
+
timer = setTimeout(() => reject(new ProcessKernelStartupTimeoutError(deadlineMs)), deadlineMs);
|
|
117
|
+
});
|
|
118
|
+
// When the deadline wins, the stopped child's readiness promise rejects later; that outcome is already reported.
|
|
119
|
+
ready.catch(() => {});
|
|
120
|
+
return Promise.race([ready, expired]).finally(() => clearTimeout(timer));
|
|
121
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { onCellRelease } from "./cell-run-context.js";
|
|
1
2
|
import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview-client.js";
|
|
2
3
|
|
|
3
4
|
// Bun constructs "chrome"-backend WebViews only on the process main thread, and this kernel runs in
|
|
@@ -5,6 +6,7 @@ import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview
|
|
|
5
6
|
// `Bun` parameter bound to a stand-in whose `WebView` forwards Chrome-backed views to the
|
|
6
7
|
// main-thread service over a private port; every other `Bun.*` member is the real one.
|
|
7
8
|
const KERNEL_BUN = Symbol.for("senpi.kernel.bun");
|
|
9
|
+
const KERNEL_PROCESS_MODE = Symbol.for("senpi.kernel.processMode");
|
|
8
10
|
|
|
9
11
|
export function bindKernelBun(source) {
|
|
10
12
|
if (!globalThis[KERNEL_BUN]) return source;
|
|
@@ -17,6 +19,10 @@ export function installKernelWebView(requestPort) {
|
|
|
17
19
|
globalThis[KERNEL_BUN] = shadowBun(bun, createKernelWebView(bun.WebView, requestPort));
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
export function markKernelProcessMode() {
|
|
23
|
+
globalThis[KERNEL_PROCESS_MODE] = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
function shadowBun(real, WebView) {
|
|
21
27
|
return new Proxy(
|
|
22
28
|
{},
|
|
@@ -35,7 +41,10 @@ function shadowBun(real, WebView) {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
// The macOS default (WebKit) stays a native worker view: its host process serves any thread.
|
|
44
|
+
// A process-mode kernel already runs its cells on the child's main thread, so the native
|
|
45
|
+
// WebView constructs there directly and no MessagePort proxy is ever needed.
|
|
38
46
|
function needsMainThread(options) {
|
|
47
|
+
if (globalThis[KERNEL_PROCESS_MODE]) return false;
|
|
39
48
|
const backend = options?.backend;
|
|
40
49
|
if (backend === undefined) return process.platform !== "darwin";
|
|
41
50
|
return backend === "chrome" || (typeof backend === "object" && backend !== null && backend.type === "chrome");
|
|
@@ -82,13 +91,16 @@ function createKernelWebView(NativeWebView, requestPort) {
|
|
|
82
91
|
#console;
|
|
83
92
|
#ready;
|
|
84
93
|
#closed = false;
|
|
94
|
+
#forgetRelease;
|
|
85
95
|
onNavigated = null;
|
|
86
96
|
onNavigationFailed = null;
|
|
87
97
|
|
|
88
98
|
constructor(options = {}) {
|
|
89
99
|
if (!needsMainThread(options)) {
|
|
90
100
|
const view = new NativeWebView(options);
|
|
91
|
-
|
|
101
|
+
const ref = new WeakRef(view);
|
|
102
|
+
natives.add(ref);
|
|
103
|
+
onCellRelease(() => ref.deref()?.close());
|
|
92
104
|
return view;
|
|
93
105
|
}
|
|
94
106
|
super();
|
|
@@ -96,6 +108,7 @@ function createKernelWebView(NativeWebView, requestPort) {
|
|
|
96
108
|
this.#console = capture;
|
|
97
109
|
proxied.add(this);
|
|
98
110
|
live.add(this);
|
|
111
|
+
this.#forgetRelease = onCellRelease(() => this.close());
|
|
99
112
|
const viewId = this.#viewId;
|
|
100
113
|
this.#ready = connect().then(async client => {
|
|
101
114
|
client.register(viewId, this);
|
|
@@ -149,6 +162,7 @@ function createKernelWebView(NativeWebView, requestPort) {
|
|
|
149
162
|
close() {
|
|
150
163
|
if (this.#closed) return;
|
|
151
164
|
this.#closed = true;
|
|
165
|
+
this.#forgetRelease?.();
|
|
152
166
|
live.delete(this);
|
|
153
167
|
const viewId = this.#viewId;
|
|
154
168
|
this.#ready.then(
|
|
@@ -1,42 +1,19 @@
|
|
|
1
1
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
-
|
|
4
|
-
type HostCellOutcome = Awaited<ReturnType<HostCellExecutor>>;
|
|
5
|
-
|
|
3
|
+
import { runHostCell as runSharedHostCell } from "../shared/host-cell.ts";
|
|
6
4
|
import type { PendingRun, ResultMessage } from "./kernel-contract.ts";
|
|
7
|
-
import { failedPythonResult } from "./transport.ts";
|
|
8
5
|
|
|
9
6
|
export function runHostCell(
|
|
10
7
|
pending: PendingRun,
|
|
11
8
|
host: HostCellExecutor,
|
|
12
9
|
io: { readonly emit: (message: KernelToHostMessage) => void; readonly settle: (result: ResultMessage) => void },
|
|
13
10
|
): void {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const emit = (message: KernelToHostMessage): void => {
|
|
20
|
-
if (!finished && !abort.signal.aborted) io.emit(message);
|
|
21
|
-
};
|
|
22
|
-
// Calling the executor inside a promise keeps a synchronous throw in this cell instead of failing the queue.
|
|
23
|
-
const done = new Promise<HostCellOutcome>((resolve) => resolve(host({ signal: abort.signal, emit }))).then(
|
|
24
|
-
(outcome): ResultMessage =>
|
|
25
|
-
outcome.ok
|
|
26
|
-
? {
|
|
27
|
-
type: "result",
|
|
28
|
-
cellId,
|
|
29
|
-
ok: true,
|
|
30
|
-
durationMs: 0,
|
|
31
|
-
...(outcome.valueRepr === undefined ? {} : { valueRepr: outcome.valueRepr }),
|
|
32
|
-
}
|
|
33
|
-
: { type: "result", cellId, ok: false, error: outcome.error, durationMs: 0 },
|
|
34
|
-
(error: unknown): ResultMessage =>
|
|
35
|
-
failedPythonResult(cellId, error instanceof Error ? error.message : String(error)),
|
|
36
|
-
);
|
|
37
|
-
pending.hostDone = done;
|
|
38
|
-
void done.then((result) => {
|
|
39
|
-
finished = true;
|
|
40
|
-
if (!abort.signal.aborted) io.settle(result);
|
|
11
|
+
const run = runSharedHostCell(pending.input.cellId, host, {
|
|
12
|
+
emit: io.emit,
|
|
13
|
+
settle: io.settle,
|
|
14
|
+
durationMs: () => 0,
|
|
15
|
+
settleAfterAbort: false,
|
|
41
16
|
});
|
|
17
|
+
pending.hostAbort = run.abort;
|
|
18
|
+
pending.hostDone = run.done;
|
|
42
19
|
}
|