@code-yeongyu/senpi-codemode 2026.10.10 → 2026.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/README.md +113 -11
- package/package.json +6 -5
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +9 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +52 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/editable-check.ts +1 -1
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +42 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +20 -6
- package/src/index.ts +2 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/globals.jl +128 -0
- package/src/kernels/jl/prelude.jl +15 -2
- package/src/kernels/jl/runner.jl +35 -143
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +23 -24
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/inline-worker.ts +2 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +8 -0
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-tree-host.ts +4 -8
- package/src/kernels/js/process-tree.d.ts +11 -0
- package/src/kernels/js/process-tree.js +55 -10
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/windows-tree-kill-host.ts +117 -0
- package/src/kernels/js/windows-tree-kill.d.ts +19 -0
- package/src/kernels/js/windows-tree-kill.js +100 -0
- package/src/kernels/js/worker-core.js +51 -8
- package/src/kernels/js/worker-cwd.js +46 -12
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +66 -13
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +9 -0
- package/src/kernels/js/worker-slot.ts +8 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/prelude.py +69 -27
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/sandbox-cell.ts +26 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/shared/bun-ffi.ts +24 -0
- package/src/kernels/shared/kernel-memory-host.ts +30 -7
- package/src/kernels/shared/process-cpu.ts +7 -3
- package/src/kernels/shared/process-group-cpu.ts +6 -3
- package/src/kernels/shared/subprocess-kernel.ts +29 -3
- package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
- package/src/kernels/shared/subprocess-process.ts +24 -2
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +3 -0
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-tool.ts +4 -2
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render-agent.ts +110 -0
- package/src/tool/render-blocks.ts +252 -0
- package/src/tool/render-call.ts +130 -0
- package/src/tool/render-cell.ts +165 -0
- package/src/tool/render-detail.ts +152 -0
- package/src/tool/render-live.ts +288 -0
- package/src/tool/render-result.ts +202 -0
- package/src/tool/render-status.ts +166 -0
- package/src/tool/render-tail.ts +139 -0
- package/src/tool/render.ts +3 -1069
- package/src/tool/run-eval-cell.ts +24 -14
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +13 -2
|
@@ -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,9 +1,9 @@
|
|
|
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
3
|
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
5
4
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
6
5
|
import { dispatchCell } from "./cell-dispatch.ts";
|
|
6
|
+
import { consumeControlFrame } from "./control-frames.ts";
|
|
7
7
|
import { HostEntries } from "./host-entries.ts";
|
|
8
8
|
import { DEFAULT_INTERRUPT_BOUNDS, JS_INTERRUPT_GRACE_MS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
9
9
|
import {
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type ResultMessage,
|
|
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,
|
|
@@ -25,7 +26,6 @@ import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-
|
|
|
25
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
|
|
|
@@ -57,7 +57,7 @@ export class JavaScriptKernel {
|
|
|
57
57
|
);
|
|
58
58
|
readonly #toolCalls = new ToolCallQueue();
|
|
59
59
|
readonly #children: WorkerChildren;
|
|
60
|
-
readonly #memory:
|
|
60
|
+
readonly #memory: JavaScriptKernelMemory;
|
|
61
61
|
readonly #activeCell: ActiveCellControl;
|
|
62
62
|
readonly #recovery = new WorkerRecovery({
|
|
63
63
|
runs: this.#runs,
|
|
@@ -76,7 +76,7 @@ export class JavaScriptKernel {
|
|
|
76
76
|
recover: () => void this.#recovery.recover(() => Promise.resolve()),
|
|
77
77
|
clearToolCalls: () => this.#toolCalls.clear(),
|
|
78
78
|
});
|
|
79
|
-
this.#memory = new
|
|
79
|
+
this.#memory = new JavaScriptKernelMemory(options, () => this.#slot.processPid);
|
|
80
80
|
this.#children = new WorkerChildren(options.collectOrphanedChildren);
|
|
81
81
|
this.#moduleLoader = new LocalModuleLoader(options);
|
|
82
82
|
this.#slot = new WorkerSlot(options, {
|
|
@@ -90,15 +90,20 @@ export class JavaScriptKernel {
|
|
|
90
90
|
return this.#slot.mode;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** 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. */
|
|
94
99
|
get lastLiveBytes(): number | undefined {
|
|
95
100
|
return this.#memory.lastLiveBytes;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/** A heap reading taken between cells without running one; nothing when no worker is live to ask. */
|
|
99
104
|
async queryMemory(): Promise<JavaScriptMemoryReading | undefined> {
|
|
100
|
-
|
|
101
|
-
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);
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
get kernelToolEvents(): EventTarget {
|
|
@@ -202,7 +207,13 @@ export class JavaScriptKernel {
|
|
|
202
207
|
|
|
203
208
|
async #ensureReady(): Promise<void> {
|
|
204
209
|
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
205
|
-
|
|
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
|
+
}
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
#startNext(): void {
|
|
@@ -224,15 +235,7 @@ export class JavaScriptKernel {
|
|
|
224
235
|
|
|
225
236
|
#handleMessage(message: KernelToHostMessage): void {
|
|
226
237
|
if (this.#kernelTools.consume(message) && message.type !== "tool-call") return;
|
|
227
|
-
if (this.#memory
|
|
228
|
-
if (message.type === "status" && message.event.op === INTERRUPT_ACK_OP) {
|
|
229
|
-
this.#runs.acknowledgeInterrupt(message.event);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (message.type === "status" && message.event.op === CHILD_LIFECYCLE_OP) {
|
|
233
|
-
this.#children.track(message.event);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
238
|
+
if (consumeControlFrame(message, { memory: this.#memory, runs: this.#runs, children: this.#children })) return;
|
|
236
239
|
(this.#runs.active?.input.onMessage ?? this.#options.onMessage)?.(message);
|
|
237
240
|
if (message.type === "tool-call") {
|
|
238
241
|
this.#toolCalls.push(message);
|
|
@@ -255,14 +258,10 @@ export class JavaScriptKernel {
|
|
|
255
258
|
if (!active && this.#slot.startingUp) return;
|
|
256
259
|
this.#activeCell.disarm();
|
|
257
260
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", error.message));
|
|
258
|
-
this.#memory.
|
|
261
|
+
this.#memory.crashed(error);
|
|
259
262
|
// A crash mid-install stops the install (process group killed, nothing published); it settles itself once stopped.
|
|
260
263
|
if (active && !this.#hostEntries.abort(active, `JavaScript worker crashed: ${error.message}`)) {
|
|
261
|
-
this.#runs.
|
|
262
|
-
this.#runs.settle(
|
|
263
|
-
active,
|
|
264
|
-
crashedResult(active.input.cellId, error, this.#runs.durationMs(active, performance.now())),
|
|
265
|
-
);
|
|
264
|
+
this.#runs.settleCrashed(active, error);
|
|
266
265
|
}
|
|
267
266
|
this.#toolCalls.clear();
|
|
268
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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// senpi#2995: Bun.$, node:child_process exec/execFile and the synchronous spawners leave their children in the
|
|
2
|
+
// agent's process group (Bun.$ has no such option, Bun ignores `detached` for exec/execFile, and a synchronous call
|
|
3
|
+
// keeps the terminal), so a group-wide signal from the same cell stops the agent. Such a command gets a notice; the
|
|
4
|
+
// notice only warns and never fails the cell.
|
|
5
|
+
|
|
6
|
+
// A command starts a (possibly indented) line, follows `;`, `|`, `&`, `(`, a backtick, `{`, `!`, a shell keyword
|
|
7
|
+
// (`then`, `do`, `else`) or a runner (`nohup`, `time`, `xargs`, `env`, `exec`, `sudo` with flags), or opens a `-c '...'` script; a
|
|
8
|
+
// `kill` word anywhere else (`git log --grep kill`, `echo kill ...`) is text.
|
|
9
|
+
const COMMAND_POSITION = String.raw`(?:^\s*|[;|&(\x60]\s*|\b(?:then|do|else|nohup|time|xargs|env|exec)\s+|[{!]\s+|\bsudo(?:\s+-\S+)*\s+|\s-[A-Za-z]*c\s+['"]\s*)`;
|
|
10
|
+
|
|
11
|
+
// A group target is a dash argument after the signal options: a number, `$var`, `${var}` or `$(...)`.
|
|
12
|
+
const GROUP_TARGET = String.raw`-(?:\d+|\$\$|\$\{?\w+\}?|\$\()`;
|
|
13
|
+
const SIGNAL_OPTION = String.raw`(?:-s\s+\w+|-n\s+\d+|-[A-Za-z]+\d*|-\d+)`;
|
|
14
|
+
// The first dash argument is a signal, so a group is one after `--`, after a signal option, or after a plain pid:
|
|
15
|
+
// `kill -- -<pgid>`, `kill -SIG -<pgid>`, `kill -TERM 1234 -5678`, `kill -- -$$`.
|
|
16
|
+
const PID = String.raw`[^\s\-;|&][^\s;|&]*`;
|
|
17
|
+
const KILL_GROUP = new RegExp(
|
|
18
|
+
String.raw`${COMMAND_POSITION}kill\b(?:` +
|
|
19
|
+
String.raw`\s+--\s+${GROUP_TARGET}` +
|
|
20
|
+
String.raw`|(?:\s+${SIGNAL_OPTION})+(?:\s+${PID})*\s+(?:--\s+)?${GROUP_TARGET}` +
|
|
21
|
+
String.raw`|(?:\s+${SIGNAL_OPTION})*(?:\s+${PID})+\s+${GROUP_TARGET})`,
|
|
22
|
+
"mu",
|
|
23
|
+
);
|
|
24
|
+
const PKILL_GROUP = new RegExp(String.raw`${COMMAND_POSITION}pkill\b[^\n;|&]*\s(?:-g\d*|--pgroup)\b`, "mu");
|
|
25
|
+
// `killall` in command position (also after `sudo` or as a `-c` script) signals every process with that name, which
|
|
26
|
+
// can include the agent's own (bun, node, senpi). Inside an `echo` string it is only text.
|
|
27
|
+
const KILLALL = new RegExp(String.raw`${COMMAND_POSITION}killall\s`, "mu");
|
|
28
|
+
|
|
29
|
+
export const GROUP_SIGNAL_NOTICE_TAG = "[senpi:group-signal]";
|
|
30
|
+
|
|
31
|
+
export function signalsProcessGroup(commandText) {
|
|
32
|
+
return KILL_GROUP.test(commandText) || PKILL_GROUP.test(commandText) || KILLALL.test(commandText);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function groupSignalNotice(api, readGroup = agentProcessGroup) {
|
|
36
|
+
const pgid = readGroup();
|
|
37
|
+
const group = pgid === undefined ? "the agent's process group" : `the agent's process group (${pgid})`;
|
|
38
|
+
return (
|
|
39
|
+
`${GROUP_SIGNAL_NOTICE_TAG} This command signals a process group. Children of ${api} share ${group}, so ` +
|
|
40
|
+
"signalling that group can stop this session. Start background jobs with Bun.spawn or child_process.spawn, " +
|
|
41
|
+
"which give them their own group, or signal them by pid.\n"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cachedGroup;
|
|
46
|
+
|
|
47
|
+
// A bounded `ps` lookup, kept once it succeeds; a missing `ps` or a failed run only drops the number from this notice.
|
|
48
|
+
export function agentProcessGroup(spawnSync = globalThis.Bun?.spawnSync) {
|
|
49
|
+
if (cachedGroup !== undefined) return cachedGroup;
|
|
50
|
+
let text = "";
|
|
51
|
+
try {
|
|
52
|
+
const result = spawnSync?.(["ps", "-o", "pgid=", "-p", String(process.pid)], { timeout: 2_000 });
|
|
53
|
+
text = result?.success ? String(result.stdout).trim() : "";
|
|
54
|
+
} catch {
|
|
55
|
+
text = "";
|
|
56
|
+
}
|
|
57
|
+
if (!/^\d+$/u.test(text)) return undefined;
|
|
58
|
+
cachedGroup = text;
|
|
59
|
+
return cachedGroup;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shellCommandText(strings, expressions) {
|
|
63
|
+
if (!Array.isArray(strings)) return String(strings ?? "");
|
|
64
|
+
return strings.reduce((text, part, index) => text + part + (index < expressions.length ? String(expressions[index]) : ""), "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { promisify } = process.getBuiltinModule("node:util");
|
|
68
|
+
const SCANNED_SPAWNERS = ["exec", "execFile", "execSync", "execFileSync", "spawnSync"];
|
|
69
|
+
|
|
70
|
+
function commandTextOf(name, args) {
|
|
71
|
+
if (name === "exec" || name === "execSync") return String(args[0]);
|
|
72
|
+
return [args[0], ...(Array.isArray(args[1]) ? args[1] : [])].join(" ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function noticeChildProcessGroupSignals(emitText, isActive) {
|
|
76
|
+
const childProcess = process.getBuiltinModule("node:child_process");
|
|
77
|
+
const originals = new Map();
|
|
78
|
+
for (const name of SCANNED_SPAWNERS) {
|
|
79
|
+
const original = childProcess[name];
|
|
80
|
+
if (typeof original !== "function") continue;
|
|
81
|
+
originals.set(name, original);
|
|
82
|
+
const scan = (args) => {
|
|
83
|
+
try {
|
|
84
|
+
if (isActive() && signalsProcessGroup(commandTextOf(name, args))) {
|
|
85
|
+
emitText("stderr", groupSignalNotice(`child_process.${name}`));
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// The notice is advisory; the call itself always runs.
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const scanned = function scannedSpawner(...args) {
|
|
92
|
+
scan(args);
|
|
93
|
+
return original.apply(this, args);
|
|
94
|
+
};
|
|
95
|
+
// Every own property, symbols included: `util.promisify.custom` is what makes promisify(exec) resolve to
|
|
96
|
+
// { stdout, stderr }. That custom function calls the original directly, so it is scanned too.
|
|
97
|
+
for (const key of Reflect.ownKeys(original)) {
|
|
98
|
+
if (key === "length" || key === "name" || key === "prototype" || key === promisify.custom) continue;
|
|
99
|
+
const descriptor = Object.getOwnPropertyDescriptor(original, key);
|
|
100
|
+
if (descriptor) Object.defineProperty(scanned, key, descriptor);
|
|
101
|
+
}
|
|
102
|
+
const customPromisify = original[promisify.custom];
|
|
103
|
+
if (typeof customPromisify === "function") {
|
|
104
|
+
Object.defineProperty(scanned, promisify.custom, {
|
|
105
|
+
configurable: true,
|
|
106
|
+
value: function scannedPromisified(...args) {
|
|
107
|
+
scan(args);
|
|
108
|
+
return customPromisify.apply(this, args);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
childProcess[name] = scanned;
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
for (const [name, original] of originals) childProcess[name] = original;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -19,6 +19,8 @@ export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntry
|
|
|
19
19
|
|
|
20
20
|
export interface WorkerLike {
|
|
21
21
|
readonly mode: JavaScriptKernelMode;
|
|
22
|
+
/** The child pid when `mode` is `"process"`; the slot reports it for orphan accounting after a crash. */
|
|
23
|
+
readonly pid?: number;
|
|
22
24
|
postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void;
|
|
23
25
|
onMessage(handler: (message: KernelToHostMessage) => void): () => void;
|
|
24
26
|
onError(handler: (error: Error) => void): () => void;
|
|
@@ -7,7 +7,7 @@ import type { JavaScriptInterruptBounds } from "./interrupt-bounds.ts";
|
|
|
7
7
|
export type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
|
|
8
8
|
export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
9
9
|
|
|
10
|
-
export type JavaScriptKernelMode = "worker" | "inline";
|
|
10
|
+
export type JavaScriptKernelMode = "worker" | "inline" | "process";
|
|
11
11
|
|
|
12
12
|
/** Snapshot or live provider consulted when the worker registry checks collisions. */
|
|
13
13
|
export type KernelToolNameSource = readonly string[] | (() => readonly string[]);
|
|
@@ -30,8 +30,23 @@ export interface JavaScriptKernelOptions {
|
|
|
30
30
|
readonly workerEntryUrl?: URL;
|
|
31
31
|
/** Per-session PI_* values applied to the worker environment before the first cell runs. */
|
|
32
32
|
readonly sessionEnv?: SessionEnvironment;
|
|
33
|
+
/** Opt-in child-process kernel; absent keeps the worker-thread kernel. */
|
|
34
|
+
readonly isolation?: "process";
|
|
35
|
+
/** Resolution root for the process-mode runtime (bun, then node); `""` names a machine with none on PATH. */
|
|
36
|
+
readonly processCommandPath?: string;
|
|
37
|
+
/** The host's executable, which runs the child when it is bun or node; defaults to `process.execPath`. */
|
|
38
|
+
readonly processExecPath?: string;
|
|
39
|
+
/** How long a process-mode child gets to report ready; defaults to `PROCESS_STARTUP_DEADLINE_MS`. */
|
|
40
|
+
readonly processStartupDeadlineMs?: number;
|
|
41
|
+
/** Process-mode memory policy: the host reads the child's footprint instead of the in-heap worker reading. */
|
|
42
|
+
readonly processMemory?: {
|
|
43
|
+
readonly thresholds: KernelMemoryThresholds;
|
|
44
|
+
readonly readFootprint?: (pid: number) => { readonly bytes: number } | undefined;
|
|
45
|
+
};
|
|
33
46
|
/** Host tool names denied as JS kernel-tool identifiers (init protocol). */
|
|
34
47
|
readonly hostToolNames?: KernelToolNameSource;
|
|
48
|
+
/** `kernelTools.enabled`; false makes `tool(fn)` refuse with `tools_unavailable`. Unset keeps them on. */
|
|
49
|
+
readonly kernelToolsEnabled?: boolean;
|
|
35
50
|
/** Tool names registered in another kernel language, denied as JS kernel-tool identifiers. */
|
|
36
51
|
readonly foreignLanguageNames?: KernelToolNameSource;
|
|
37
52
|
/** Post-cell collection, notice, and ceiling thresholds; absent leaves the worker's memory unmanaged. */
|
|
@@ -6,7 +6,7 @@ import type { ResultMessage } from "./kernel-contract.ts";
|
|
|
6
6
|
|
|
7
7
|
export interface JavaScriptMemoryReading {
|
|
8
8
|
readonly liveBytes: number;
|
|
9
|
-
readonly measure: "heap";
|
|
9
|
+
readonly measure: "heap" | "footprint";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** A query waits at most this long: the worker answers between cells, never inside one. */
|