@code-yeongyu/senpi-codemode 2026.10.10 → 2026.10.12
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 +214 -0
- package/README.md +113 -11
- package/package.json +7 -6
- 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 +80 -44
- package/src/environments/python-environments.ts +1 -1
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-cells-rpc.ts +62 -0
- 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 +8 -3
- 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 +20 -11
- 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 +21 -10
- 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
|
@@ -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(
|
|
@@ -61,6 +61,7 @@ RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
|
61
61
|
RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
62
62
|
RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
63
63
|
RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
64
|
+
RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__"
|
|
64
65
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
65
66
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
66
67
|
|
|
@@ -660,25 +661,6 @@ class ThreadRoutedStream(io.TextIOBase):
|
|
|
660
661
|
)
|
|
661
662
|
self.cell_target: io.StringIO | None = None
|
|
662
663
|
|
|
663
|
-
@contextlib.contextmanager
|
|
664
|
-
def capture(self, buffer: io.StringIO, *, cell: bool) -> Iterator[None]:
|
|
665
|
-
me = threading.get_ident()
|
|
666
|
-
previous = self._targets.get(me)
|
|
667
|
-
self._targets[me] = buffer
|
|
668
|
-
inherited = self._inherited.set(buffer)
|
|
669
|
-
if cell:
|
|
670
|
-
self.cell_target = buffer
|
|
671
|
-
try:
|
|
672
|
-
yield
|
|
673
|
-
finally:
|
|
674
|
-
self._inherited.reset(inherited)
|
|
675
|
-
if previous is None:
|
|
676
|
-
self._targets.pop(me, None)
|
|
677
|
-
else:
|
|
678
|
-
self._targets[me] = previous
|
|
679
|
-
if cell:
|
|
680
|
-
self.cell_target = None
|
|
681
|
-
|
|
682
664
|
def write(self, text: str) -> int:
|
|
683
665
|
target = self._targets.get(threading.get_ident()) or self._inherited.get() or self.cell_target
|
|
684
666
|
if target is None:
|
|
@@ -693,6 +675,38 @@ class ThreadRoutedStream(io.TextIOBase):
|
|
|
693
675
|
return None
|
|
694
676
|
|
|
695
677
|
|
|
678
|
+
@contextlib.contextmanager
|
|
679
|
+
def capture_streams(
|
|
680
|
+
streams: tuple[ThreadRoutedStream, ThreadRoutedStream],
|
|
681
|
+
buffers: tuple[io.StringIO, io.StringIO],
|
|
682
|
+
*,
|
|
683
|
+
cell: bool,
|
|
684
|
+
) -> Iterator[None]:
|
|
685
|
+
stdout, stderr = streams
|
|
686
|
+
out, err = buffers
|
|
687
|
+
me = threading.get_ident()
|
|
688
|
+
previous_out, previous_err = stdout._targets.get(me), stderr._targets.get(me)
|
|
689
|
+
stdout._targets[me], stderr._targets[me] = out, err
|
|
690
|
+
inherited_out, inherited_err = stdout._inherited.set(out), stderr._inherited.set(err)
|
|
691
|
+
if cell:
|
|
692
|
+
stdout.cell_target, stderr.cell_target = out, err
|
|
693
|
+
try:
|
|
694
|
+
yield
|
|
695
|
+
finally:
|
|
696
|
+
stderr._inherited.reset(inherited_err)
|
|
697
|
+
stdout._inherited.reset(inherited_out)
|
|
698
|
+
if previous_err is None:
|
|
699
|
+
stderr._targets.pop(me, None)
|
|
700
|
+
else:
|
|
701
|
+
stderr._targets[me] = previous_err
|
|
702
|
+
if previous_out is None:
|
|
703
|
+
stdout._targets.pop(me, None)
|
|
704
|
+
else:
|
|
705
|
+
stdout._targets[me] = previous_out
|
|
706
|
+
if cell:
|
|
707
|
+
stdout.cell_target, stderr.cell_target = None, None
|
|
708
|
+
|
|
709
|
+
|
|
696
710
|
@dataclass
|
|
697
711
|
class _Entry:
|
|
698
712
|
name: str
|
|
@@ -704,7 +718,8 @@ class _Entry:
|
|
|
704
718
|
|
|
705
719
|
|
|
706
720
|
class Registry:
|
|
707
|
-
def __init__(self, on_change: Callable[[list[str]], None]) -> None:
|
|
721
|
+
def __init__(self, on_change: Callable[[list[str]], None], *, disabled: bool = False) -> None:
|
|
722
|
+
self._disabled = disabled
|
|
708
723
|
self._lock = threading.Lock()
|
|
709
724
|
self._entries: dict[str, _Entry] = {}
|
|
710
725
|
# Revisions outlive undefine: a redefined name never reuses a revision an old descriptor carries.
|
|
@@ -713,6 +728,8 @@ class Registry:
|
|
|
713
728
|
self.generation = 1
|
|
714
729
|
|
|
715
730
|
def define(self, fn: Any, *, name: Any = None, description: Any = None, schema: Any = None) -> Any:
|
|
731
|
+
if self._disabled:
|
|
732
|
+
raise KernelToolError("tools_unavailable", "kernel tools are turned off for this project (kernelTools.enabled is false)")
|
|
716
733
|
inferred = infer_tool(fn, name=name, description=description, schema=schema)
|
|
717
734
|
key = tool_key(inferred["name"])
|
|
718
735
|
with self._lock:
|
|
@@ -901,8 +918,7 @@ class KernelToolRunner:
|
|
|
901
918
|
if invocation.cancelled.is_set():
|
|
902
919
|
raise Cancelled(CANCELLED_BEFORE_START)
|
|
903
920
|
CURRENT.set(invocation)
|
|
904
|
-
|
|
905
|
-
with stdout.capture(output, cell=False), stderr.capture(output, cell=False):
|
|
921
|
+
with capture_streams(self._streams, (output, output), cell=False):
|
|
906
922
|
value = self._call(invocation, entry.fn, args)
|
|
907
923
|
finally:
|
|
908
924
|
CURRENT.set(None)
|
|
@@ -983,7 +999,12 @@ async def _awaited(awaitable: Any) -> Any:
|
|
|
983
999
|
|
|
984
1000
|
KERNEL_TOOL_TOKEN = OwnerToken()
|
|
985
1001
|
KERNEL_TOOL_STREAMS = (ThreadRoutedStream(stderr=False), ThreadRoutedStream(stderr=True))
|
|
986
|
-
|
|
1002
|
+
# kernelTools.enabled is read once, while the prelude loads and before any cell runs, then removed from the
|
|
1003
|
+
# environment: rewriting os.environ cannot turn kernel tools back on, and cell subprocesses never see the switch.
|
|
1004
|
+
KERNEL_TOOL_REGISTRY = Registry(
|
|
1005
|
+
lambda names: emit({"type": "kernel-tools-defined", "names": names}),
|
|
1006
|
+
disabled=os.environ.pop("SENPI_CODEMODE_KERNEL_TOOLS", None) == "0",
|
|
1007
|
+
)
|
|
987
1008
|
KERNEL_TOOL_RUNNER = KernelToolRunner(emit, KERNEL_TOOL_TOKEN, KERNEL_TOOL_REGISTRY, KERNEL_TOOL_STREAMS)
|
|
988
1009
|
|
|
989
1010
|
|
|
@@ -1154,6 +1175,27 @@ def tool_schema(name: str | None = None) -> Any:
|
|
|
1154
1175
|
)
|
|
1155
1176
|
|
|
1156
1177
|
|
|
1178
|
+
class _Packages:
|
|
1179
|
+
"""packages.install(manager, requirements, *, timeout=600): the %pip installer as a call; returns its receipt."""
|
|
1180
|
+
|
|
1181
|
+
__slots__ = ()
|
|
1182
|
+
|
|
1183
|
+
def install(self, manager: str, requirements: Any, *, timeout: float | None = None) -> Any:
|
|
1184
|
+
args: dict[str, Any] = {"manager": manager, "requirements": requirements}
|
|
1185
|
+
if timeout is not None:
|
|
1186
|
+
args["timeout"] = timeout
|
|
1187
|
+
# A long install must not hit the 60 s socket cap; the host bounds it by the timeout (600 s by default).
|
|
1188
|
+
socket_timeout = (600 if timeout is None else float(timeout)) + _WAIT_SOCKET_GRACE_SECONDS
|
|
1189
|
+
return bridge_post(
|
|
1190
|
+
"/call",
|
|
1191
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_PACKAGES_INSTALL_TOOL, "args": args},
|
|
1192
|
+
socket_timeout=socket_timeout,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
packages = _Packages()
|
|
1197
|
+
|
|
1198
|
+
|
|
1157
1199
|
def output(
|
|
1158
1200
|
*ids: str,
|
|
1159
1201
|
format: str = "raw",
|
|
@@ -1781,6 +1823,7 @@ USER_NS.update(
|
|
|
1781
1823
|
"workpool": workpool,
|
|
1782
1824
|
"output": output,
|
|
1783
1825
|
"tool_schema": tool_schema,
|
|
1826
|
+
"packages": packages,
|
|
1784
1827
|
"wait": wait,
|
|
1785
1828
|
"handle": handle,
|
|
1786
1829
|
"__senpi_magic": _magic,
|
|
@@ -2187,8 +2230,7 @@ def run_cell(
|
|
|
2187
2230
|
cell_scope = CURRENT_CELL_TOKEN.set(cell_token)
|
|
2188
2231
|
try:
|
|
2189
2232
|
KERNEL_TOOL_TOKEN.acquire()
|
|
2190
|
-
|
|
2191
|
-
with cell_stdout.capture(stdout, cell=True), cell_stderr.capture(stderr, cell=True):
|
|
2233
|
+
with capture_streams(KERNEL_TOOL_STREAMS, (stdout, stderr), cell=True):
|
|
2192
2234
|
apply_preludes(preludes)
|
|
2193
2235
|
leave_source_file = _enter_source_file(source_file)
|
|
2194
2236
|
body, expression = compile_cell(code, source_file or "<cell>")
|
|
@@ -2342,7 +2384,7 @@ def _start_parent_watch() -> None:
|
|
|
2342
2384
|
).start()
|
|
2343
2385
|
|
|
2344
2386
|
|
|
2345
|
-
def _read_control(commands: queue.
|
|
2387
|
+
def _read_control(commands: queue.SimpleQueue[tuple[str, Any]]) -> None:
|
|
2346
2388
|
# Kernel-tool frames are served here, so a callback can be admitted while the main thread is inside a cell.
|
|
2347
2389
|
for raw in sys.stdin:
|
|
2348
2390
|
try:
|
|
@@ -2361,7 +2403,7 @@ def main() -> None:
|
|
|
2361
2403
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
2362
2404
|
_start_parent_watch()
|
|
2363
2405
|
sys.stdout, sys.stderr = KERNEL_TOOL_STREAMS
|
|
2364
|
-
commands: queue.
|
|
2406
|
+
commands: queue.SimpleQueue[tuple[str, Any]] = queue.SimpleQueue()
|
|
2365
2407
|
threading.Thread(target=_read_control, args=(commands,), name="senpi-control-reader", daemon=True).start()
|
|
2366
2408
|
host_closed = False
|
|
2367
2409
|
while True:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
2
4
|
import type { Readable, Writable } from "node:stream";
|
|
3
5
|
|
|
4
6
|
export interface KernelChild {
|
|
@@ -53,6 +55,8 @@ export function defaultSpawn(options: KernelSpawnOptions): KernelChild {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export function splitCommand(commandLine: string): { readonly command: string; readonly args: readonly string[] } {
|
|
58
|
+
// A configured interpreter path is one executable even when it contains spaces.
|
|
59
|
+
if (isAbsolute(commandLine) && existsSync(commandLine)) return { command: commandLine, args: [] };
|
|
56
60
|
const [command, ...args] = commandLine.split(" ").filter(Boolean);
|
|
57
61
|
if (!command) throw new Error("Python interpreter path is empty");
|
|
58
62
|
return { command, args };
|
|
@@ -244,6 +244,10 @@ export class PythonKernelTransport {
|
|
|
244
244
|
this.#child.stderr.on("data", onStderr);
|
|
245
245
|
this.#child.on("error", onError);
|
|
246
246
|
this.#child.on("exit", onExit);
|
|
247
|
+
// senpi#3016: a frame written while the interpreter is dying fails on the stdin stream (EPIPE), not on the child.
|
|
248
|
+
// #onError already ignores it once the child has exited or the transport is inactive, and the listener is never
|
|
249
|
+
// detached, so a late write failure is never unhandled.
|
|
250
|
+
this.#child.stdin.on("error", onError);
|
|
247
251
|
const { sessionId, connection, memory, kernelGeneration } = this.#options;
|
|
248
252
|
this.#write({
|
|
249
253
|
type: "init",
|
package/src/kernels/rb/runner.rb
CHANGED
|
@@ -9,6 +9,7 @@ $stdout.flush
|
|
|
9
9
|
$__senpi_binding = TOPLEVEL_BINDING
|
|
10
10
|
$__senpi_frame_mutex = Mutex.new
|
|
11
11
|
$__senpi_current_cell = nil
|
|
12
|
+
$__senpi_memory_cell = nil
|
|
12
13
|
$__senpi_capture_cell = nil
|
|
13
14
|
$__senpi_connection = nil
|
|
14
15
|
$__senpi_frame_io = STDOUT.dup
|
|
@@ -123,8 +124,6 @@ rescue JSON::GeneratorError
|
|
|
123
124
|
value.inspect
|
|
124
125
|
end
|
|
125
126
|
|
|
126
|
-
$__senpi_memory = nil
|
|
127
|
-
|
|
128
127
|
SENPI_SIZER_SAMPLE = 1_000
|
|
129
128
|
SENPI_SIZER_NODE_BUDGET = 5_000
|
|
130
129
|
SENPI_SIZER_MAX_DEPTH = 64
|
|
@@ -239,19 +238,6 @@ rescue StandardError
|
|
|
239
238
|
[]
|
|
240
239
|
end
|
|
241
240
|
|
|
242
|
-
# The host reads the interpreter's footprint and decides on the notice; the runner only names its
|
|
243
|
-
# largest globals. Nothing here spawns a process, so the user's `$?` is left alone.
|
|
244
|
-
def __senpi_memory_report
|
|
245
|
-
return nil if $__senpi_memory.nil?
|
|
246
|
-
named = __senpi_largest_globals(5)
|
|
247
|
-
# The host replaces liveBytes with the interpreter footprint it reads itself; only globals is used.
|
|
248
|
-
report = { "liveBytes" => 0, "measure" => "footprint" }
|
|
249
|
-
report["globals"] = named unless named.empty?
|
|
250
|
-
report
|
|
251
|
-
rescue StandardError
|
|
252
|
-
nil
|
|
253
|
-
end
|
|
254
|
-
|
|
255
241
|
SENPI_NON_DISPLAY_NODES = %i[
|
|
256
242
|
LASGN IASGN GASGN CVASGN DASGN OP_ASGN OP_CDECL CDECL MASGN CASGN
|
|
257
243
|
DEFN DEFS CLASS MODULE SCLASS ALIAS UNDEF
|
|
@@ -305,8 +291,6 @@ def __senpi_run_cell(message)
|
|
|
305
291
|
"durationMs" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
|
|
306
292
|
}
|
|
307
293
|
frame["valueRepr"] = __senpi_value_repr(value) if !value.nil? && __senpi_should_display_result?(source)
|
|
308
|
-
memory = __senpi_memory_report
|
|
309
|
-
frame["memory"] = memory unless memory.nil?
|
|
310
294
|
__senpi_emit(frame)
|
|
311
295
|
rescue Exception => error
|
|
312
296
|
__senpi_emit({
|
|
@@ -319,6 +303,7 @@ def __senpi_run_cell(message)
|
|
|
319
303
|
ensure
|
|
320
304
|
$__senpi_capture_cell = nil
|
|
321
305
|
$__senpi_current_cell = nil
|
|
306
|
+
$__senpi_memory_cell = cell_id
|
|
322
307
|
end
|
|
323
308
|
end
|
|
324
309
|
|
|
@@ -332,10 +317,15 @@ $__senpi_protocol_stdin.each_line do |line|
|
|
|
332
317
|
when "init"
|
|
333
318
|
__senpi_emit({ "type" => "status", "event" => { "op" => "kernel-startup", "stage" => "host-init" } })
|
|
334
319
|
$__senpi_connection = message["connection"]
|
|
335
|
-
|
|
336
|
-
__senpi_emit({ "type" => "ready" })
|
|
320
|
+
__senpi_emit({ "type" => "ready", "memoryGlobals" => true })
|
|
337
321
|
when "run"
|
|
322
|
+
$__senpi_memory_cell = nil
|
|
338
323
|
__senpi_run_cell(message)
|
|
324
|
+
when "memory-globals"
|
|
325
|
+
if message["cellId"] == $__senpi_memory_cell && $__senpi_current_cell.nil?
|
|
326
|
+
__senpi_emit({ "type" => "memory-globals-result", "cellId" => message["cellId"], "globals" => __senpi_largest_globals(5) })
|
|
327
|
+
$__senpi_memory_cell = nil
|
|
328
|
+
end
|
|
339
329
|
when "close"
|
|
340
330
|
__senpi_emit({ "type" => "closed" })
|
|
341
331
|
exit 0
|