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