@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10-10
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 +178 -0
- package/README.md +212 -14
- package/package.json +6 -5
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/protocol.ts +12 -3
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +48 -0
- package/src/bridges/eval-virtual-schemas.ts +12 -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/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +68 -4
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +38 -7
- package/src/extension/bridge-tool-call.ts +7 -1
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +53 -3
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +26 -4
- package/src/index.ts +4 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +48 -39
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/host-entries.ts +62 -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 +21 -5
- 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-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/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +45 -9
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +5 -0
- package/src/kernels/js/worker-slot.ts +9 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/kernel.ts +8 -2
- package/src/kernels/py/prelude.py +145 -11
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +13 -1
- package/src/kernels/rb/workpool.rb +7 -1
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/cell-source-at-start.ts +18 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/subprocess-process.ts +14 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +13 -12
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +6 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +88 -0
- package/src/tool/magic-cell-host.ts +123 -0
- package/src/tool/magic-cells.ts +75 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +73 -1
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +50 -1
|
@@ -1,19 +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
3
|
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
5
4
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
6
|
-
import {
|
|
5
|
+
import { dispatchCell } from "./cell-dispatch.ts";
|
|
6
|
+
import { consumeControlFrame } from "./control-frames.ts";
|
|
7
|
+
import { HostEntries } from "./host-entries.ts";
|
|
8
|
+
import { DEFAULT_INTERRUPT_BOUNDS, JS_INTERRUPT_GRACE_MS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
7
9
|
import {
|
|
8
10
|
assertJavaScriptKernelOpen,
|
|
9
11
|
type JavaScriptKernelMode,
|
|
10
12
|
type JavaScriptRunInput,
|
|
11
13
|
type LifecycleState,
|
|
12
14
|
type ResultMessage,
|
|
13
|
-
resolveKernelToolNameSource,
|
|
14
15
|
type ToolCallMessage,
|
|
15
16
|
} from "./kernel-contract.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { JavaScriptKernelMemory } from "./kernel-memory.ts";
|
|
18
|
+
import type { JavaScriptMemoryReading } from "./kernel-memory-bridge.ts";
|
|
17
19
|
import { kernelToolError } from "./kernel-tools-errors.ts";
|
|
18
20
|
import type {
|
|
19
21
|
KernelToolsDescribeResult,
|
|
@@ -21,10 +23,9 @@ import type {
|
|
|
21
23
|
KernelToolsInvokeRequest,
|
|
22
24
|
} from "./kernel-tools-types.ts";
|
|
23
25
|
import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
|
|
24
|
-
import { JavaScriptRunQueue } from "./run-queue.ts";
|
|
26
|
+
import { JavaScriptRunQueue, type PendingJavaScriptRun } from "./run-queue.ts";
|
|
25
27
|
import { ToolCallQueue } from "./tool-call-queue.ts";
|
|
26
28
|
import { WorkerChildren } from "./worker-children.ts";
|
|
27
|
-
import { crashedResult } from "./worker-host.ts";
|
|
28
29
|
import { WorkerRecovery } from "./worker-recovery.ts";
|
|
29
30
|
import { WorkerSlot } from "./worker-slot.ts";
|
|
30
31
|
|
|
@@ -34,6 +35,15 @@ export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
|
34
35
|
export { type JavaScriptWorkerEntryUrlOptions, resolveJsWorkerEntryUrl } from "./worker-startup.ts";
|
|
35
36
|
|
|
36
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
|
+
});
|
|
37
47
|
readonly #options: JavaScriptKernelOptions;
|
|
38
48
|
readonly #moduleLoader: LocalModuleLoader;
|
|
39
49
|
readonly #slot: WorkerSlot;
|
|
@@ -47,7 +57,7 @@ export class JavaScriptKernel {
|
|
|
47
57
|
);
|
|
48
58
|
readonly #toolCalls = new ToolCallQueue();
|
|
49
59
|
readonly #children: WorkerChildren;
|
|
50
|
-
readonly #memory:
|
|
60
|
+
readonly #memory: JavaScriptKernelMemory;
|
|
51
61
|
readonly #activeCell: ActiveCellControl;
|
|
52
62
|
readonly #recovery = new WorkerRecovery({
|
|
53
63
|
runs: this.#runs,
|
|
@@ -66,7 +76,7 @@ export class JavaScriptKernel {
|
|
|
66
76
|
recover: () => void this.#recovery.recover(() => Promise.resolve()),
|
|
67
77
|
clearToolCalls: () => this.#toolCalls.clear(),
|
|
68
78
|
});
|
|
69
|
-
this.#memory = new
|
|
79
|
+
this.#memory = new JavaScriptKernelMemory(options, () => this.#slot.processPid);
|
|
70
80
|
this.#children = new WorkerChildren(options.collectOrphanedChildren);
|
|
71
81
|
this.#moduleLoader = new LocalModuleLoader(options);
|
|
72
82
|
this.#slot = new WorkerSlot(options, {
|
|
@@ -80,15 +90,20 @@ export class JavaScriptKernel {
|
|
|
80
90
|
return this.#slot.mode;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
/** 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. */
|
|
84
99
|
get lastLiveBytes(): number | undefined {
|
|
85
100
|
return this.#memory.lastLiveBytes;
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
/** A heap reading taken between cells without running one; nothing when no worker is live to ask. */
|
|
89
104
|
async queryMemory(): Promise<JavaScriptMemoryReading | undefined> {
|
|
90
|
-
|
|
91
|
-
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);
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
get kernelToolEvents(): EventTarget {
|
|
@@ -128,6 +143,7 @@ export class JavaScriptKernel {
|
|
|
128
143
|
const cancelled = this.cancelQueued(cellId, reason);
|
|
129
144
|
return { stateRetained: Promise.resolve(true), ...(cancelled ? {} : { note: "cell not found" }) };
|
|
130
145
|
}
|
|
146
|
+
if (active && this.#hostEntries.abort(active, reason)) return { stateRetained: Promise.resolve(true) };
|
|
131
147
|
if (!active) {
|
|
132
148
|
// A worker still stuck in startup is not a healthy idle worker: retiring it is the only recovery.
|
|
133
149
|
const wedgedInStartup = this.#slot.startingUp;
|
|
@@ -162,11 +178,14 @@ export class JavaScriptKernel {
|
|
|
162
178
|
if (this.#closePromise) return await this.#closePromise;
|
|
163
179
|
this.#slot.postMessage({ type: "close" });
|
|
164
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);
|
|
165
183
|
this.#runs.settleAll("JS kernel closed");
|
|
166
184
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", "JS kernel closed"));
|
|
167
185
|
this.#toolCalls.clear();
|
|
168
186
|
const recovery = this.#recovery.inFlight;
|
|
169
187
|
const closePromise = (async () => {
|
|
188
|
+
await hostEntriesStopped;
|
|
170
189
|
if (recovery) await recovery;
|
|
171
190
|
await this.#terminate();
|
|
172
191
|
})().finally(() => {
|
|
@@ -188,25 +207,26 @@ export class JavaScriptKernel {
|
|
|
188
207
|
|
|
189
208
|
async #ensureReady(): Promise<void> {
|
|
190
209
|
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
191
|
-
|
|
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
|
+
}
|
|
192
217
|
}
|
|
193
218
|
|
|
194
219
|
#startNext(): void {
|
|
195
220
|
if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
|
|
196
221
|
const next = this.#runs.startNext(performance.now());
|
|
197
222
|
if (!next) return;
|
|
223
|
+
const dispatch = dispatchCell(next.input, this.#moduleLoader, this.#options);
|
|
224
|
+
if (dispatch.kind === "host") {
|
|
225
|
+
this.#hostEntries.start(next, dispatch.host);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
198
228
|
this.#activeCell.arm(next);
|
|
199
|
-
this.#slot.postMessage(
|
|
200
|
-
type: "kernel-tools-names",
|
|
201
|
-
hostToolNames: resolveKernelToolNameSource(this.#options.hostToolNames),
|
|
202
|
-
foreignLanguageNames: resolveKernelToolNameSource(this.#options.foreignLanguageNames),
|
|
203
|
-
});
|
|
204
|
-
this.#slot.postMessage({
|
|
205
|
-
type: "run",
|
|
206
|
-
cellId: next.input.cellId,
|
|
207
|
-
code: this.#moduleLoader.prepareCell(next.input.code, next.input.kernelPreludes),
|
|
208
|
-
timeoutMs: next.input.timeoutMs,
|
|
209
|
-
});
|
|
229
|
+
for (const frame of dispatch.frames) this.#slot.postMessage(frame);
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
async #restartAfterStop(): Promise<void> {
|
|
@@ -215,15 +235,7 @@ export class JavaScriptKernel {
|
|
|
215
235
|
|
|
216
236
|
#handleMessage(message: KernelToHostMessage): void {
|
|
217
237
|
if (this.#kernelTools.consume(message) && message.type !== "tool-call") return;
|
|
218
|
-
if (this.#memory
|
|
219
|
-
if (message.type === "status" && message.event.op === INTERRUPT_ACK_OP) {
|
|
220
|
-
this.#runs.acknowledgeInterrupt(message.event);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (message.type === "status" && message.event.op === CHILD_LIFECYCLE_OP) {
|
|
224
|
-
this.#children.track(message.event);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
238
|
+
if (consumeControlFrame(message, { memory: this.#memory, runs: this.#runs, children: this.#children })) return;
|
|
227
239
|
(this.#runs.active?.input.onMessage ?? this.#options.onMessage)?.(message);
|
|
228
240
|
if (message.type === "tool-call") {
|
|
229
241
|
this.#toolCalls.push(message);
|
|
@@ -246,13 +258,10 @@ export class JavaScriptKernel {
|
|
|
246
258
|
if (!active && this.#slot.startingUp) return;
|
|
247
259
|
this.#activeCell.disarm();
|
|
248
260
|
this.#kernelTools.rejectAll(kernelToolError("kernel_tool_stale", error.message));
|
|
249
|
-
this.#memory.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.#runs.
|
|
253
|
-
active,
|
|
254
|
-
crashedResult(active.input.cellId, error, this.#runs.durationMs(active, performance.now())),
|
|
255
|
-
);
|
|
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);
|
|
256
265
|
}
|
|
257
266
|
this.#toolCalls.clear();
|
|
258
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,62 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
import { runHostCell } from "../shared/host-cell.ts";
|
|
4
|
+
import type { ResultMessage } from "./kernel-contract.ts";
|
|
5
|
+
|
|
6
|
+
export interface HostEntryKernel<Run> {
|
|
7
|
+
emit(run: Run, message: KernelToHostMessage): void;
|
|
8
|
+
settle(run: Run, result: ResultMessage): void;
|
|
9
|
+
durationMs(run: Run): number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The host entries (installs) a JavaScript kernel is running: started per queue slot, stopped by interrupt and close. */
|
|
13
|
+
export class HostEntries<Run extends { readonly input: { readonly cellId: string } }> {
|
|
14
|
+
readonly #aborts = new Map<Run, AbortController>();
|
|
15
|
+
readonly #done = new Map<Run, Promise<unknown>>();
|
|
16
|
+
readonly #kernel: HostEntryKernel<Run>;
|
|
17
|
+
|
|
18
|
+
constructor(kernel: HostEntryKernel<Run>) {
|
|
19
|
+
this.#kernel = kernel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Runs `host` for `run`'s queue slot; it holds the slot like a cell but never reaches the worker. */
|
|
23
|
+
start(run: Run, host: HostCellExecutor): void {
|
|
24
|
+
const entry = runHostCell(run.input.cellId, host, {
|
|
25
|
+
emit: (message) => this.#kernel.emit(run, message),
|
|
26
|
+
settle: (result) => {
|
|
27
|
+
this.#aborts.delete(run);
|
|
28
|
+
this.#kernel.settle(run, result);
|
|
29
|
+
},
|
|
30
|
+
durationMs: () => this.#kernel.durationMs(run),
|
|
31
|
+
settleAfterAbort: true,
|
|
32
|
+
});
|
|
33
|
+
this.#aborts.set(run, entry.abort);
|
|
34
|
+
this.#done.set(run, entry.done);
|
|
35
|
+
void entry.done.finally(() => this.#done.delete(run));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Aborts `run`'s host entry; false when `run` is not one. */
|
|
39
|
+
abort(run: Run, reason: string): boolean {
|
|
40
|
+
const abort = this.#aborts.get(run);
|
|
41
|
+
if (abort === undefined) return false;
|
|
42
|
+
abort.abort(new Error(reason));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Aborts every running entry and waits for them to stop, at most `ms`: a closed kernel leaves no install running. */
|
|
47
|
+
async stopAll(reason: string, ms: number): Promise<void> {
|
|
48
|
+
const running = [...this.#done.values()];
|
|
49
|
+
for (const abort of this.#aborts.values()) abort.abort(new Error(reason));
|
|
50
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
51
|
+
await Promise.race([
|
|
52
|
+
Promise.allSettled(running),
|
|
53
|
+
new Promise<void>((resolve) => (timer = setTimeout(resolve, ms))),
|
|
54
|
+
]);
|
|
55
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A turn-time refusal (an unreadable `%load` file, a refused magic) settles in queue order as a failed host entry. */
|
|
60
|
+
export function refusedEntry(message: string): HostCellExecutor {
|
|
61
|
+
return async () => ({ ok: false, error: { message } });
|
|
62
|
+
}
|
|
@@ -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. */
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import { restartNotice } from "../shared/kernel-death.ts";
|
|
3
|
+
import type { ResultMessage } from "./kernel-contract.ts";
|
|
4
|
+
import { type JavaScriptMemoryReading, KernelMemoryBridge } from "./kernel-memory-bridge.ts";
|
|
5
|
+
import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
6
|
+
import { JavaScriptProcessMemoryHost } from "./process-memory-host.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The kernel's one view of memory: the worker's heap bridge, or in process mode the child process's footprint, plus
|
|
10
|
+
* the restart notice a crash leaves for the next result.
|
|
11
|
+
*/
|
|
12
|
+
export class JavaScriptKernelMemory {
|
|
13
|
+
readonly #bridge: KernelMemoryBridge;
|
|
14
|
+
readonly #process: JavaScriptProcessMemoryHost | null;
|
|
15
|
+
readonly #pid: () => number | undefined;
|
|
16
|
+
readonly #processMode: boolean;
|
|
17
|
+
#restartNotice: string | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(options: JavaScriptKernelOptions, pid: () => number | undefined) {
|
|
20
|
+
const processMode = options.isolation === "process";
|
|
21
|
+
this.#bridge = new KernelMemoryBridge(processMode ? undefined : options.memory, options.onMemoryCollected);
|
|
22
|
+
this.#process =
|
|
23
|
+
processMode && options.processMemory !== undefined
|
|
24
|
+
? new JavaScriptProcessMemoryHost(options.processMemory.thresholds, options.processMemory.readFootprint)
|
|
25
|
+
: null;
|
|
26
|
+
this.#pid = pid;
|
|
27
|
+
this.#processMode = processMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get lastLiveBytes(): number | undefined {
|
|
31
|
+
return this.#process === null ? this.#bridge.lastLiveBytes : this.#process.lastLiveBytes(this.#pid);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A reading between cells. The worker is asked only through `post`, given when a worker is ready to answer. */
|
|
35
|
+
async query(
|
|
36
|
+
post: ((message: HostToKernelMessage) => void) | undefined,
|
|
37
|
+
): Promise<JavaScriptMemoryReading | undefined> {
|
|
38
|
+
if (this.#process !== null) return this.#process.query(this.#pid);
|
|
39
|
+
return post === undefined ? undefined : await this.#bridge.query(post);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
consume(message: KernelToHostMessage): boolean {
|
|
43
|
+
return this.#bridge.consume(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
settled(message: ResultMessage): ResultMessage {
|
|
47
|
+
if (this.#process !== null) return this.#process.settled(message, this.#pid);
|
|
48
|
+
const settled = this.#bridge.settled(message);
|
|
49
|
+
if (this.#restartNotice === null) return settled;
|
|
50
|
+
const notice = this.#restartNotice;
|
|
51
|
+
this.#restartNotice = null;
|
|
52
|
+
return { ...settled, notice, kernelState: "restarted" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether the kernel should restart for memory now; only once nothing is running or queued (`idle`). */
|
|
56
|
+
claimRecycle(idle: boolean): boolean {
|
|
57
|
+
const worker = this.#bridge.claimRecycle(idle);
|
|
58
|
+
return this.#process?.claimRecycle(idle) === true || worker;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The worker was retired: pending readings fail. */
|
|
62
|
+
workerLost(error: Error): void {
|
|
63
|
+
this.#bridge.workerLost(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The kernel crashed: pending readings fail. In process mode the next result also says the kernel restarted (a
|
|
68
|
+
* crashed child loses every global); worker mode reports its crash as it always has, with no notice.
|
|
69
|
+
*/
|
|
70
|
+
crashed(error: Error): void {
|
|
71
|
+
this.#bridge.workerLost(error);
|
|
72
|
+
if (this.#process !== null) {
|
|
73
|
+
this.#process.workerLost();
|
|
74
|
+
this.#process.markRestarted(error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.#processMode) this.#restartNotice = restartNotice("js", error.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -45,6 +45,7 @@ export function createKernelToolRegistry(options = {}) {
|
|
|
45
45
|
return generation;
|
|
46
46
|
},
|
|
47
47
|
define(fn, metadata) {
|
|
48
|
+
if (options.disabled === true) throw kernelToolError("tools_unavailable", "kernel tools are turned off for this project (kernelTools.enabled is false)");
|
|
48
49
|
assertJs();
|
|
49
50
|
const parsed = parseToolFunction(fn);
|
|
50
51
|
const resolved = resolveToolMetadata(metadata, parsed.params);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, resolve, sep } from "node:path";
|
|
1
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
4
4
|
import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
|
|
@@ -98,7 +98,13 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
98
98
|
" const urlModule = await import('node:url');",
|
|
99
99
|
" target = urlModule.pathToFileURL(specifier).href;",
|
|
100
100
|
" }",
|
|
101
|
-
"
|
|
101
|
+
" const load = (resolved) => (options === undefined ? import(resolved) : import(resolved, options));",
|
|
102
|
+
" const resolvePackage = globalThis[Symbol.for('senpi.kernel.resolvePackage')];",
|
|
103
|
+
" if (target === specifier && typeof resolvePackage === 'function') {",
|
|
104
|
+
" const resolved = resolvePackage(specifier, [context.cwdUrl, context.packageRootUrl]);",
|
|
105
|
+
" if (resolved !== undefined) return await load(resolved);",
|
|
106
|
+
" }",
|
|
107
|
+
" return await load(target);",
|
|
102
108
|
"};",
|
|
103
109
|
].join("\n");
|
|
104
110
|
}
|
|
@@ -115,14 +121,24 @@ function contributionPrelude(plan: KernelPreludePlan): string {
|
|
|
115
121
|
|
|
116
122
|
export class LocalModuleLoader {
|
|
117
123
|
readonly #prelude: string;
|
|
124
|
+
readonly #cwdUrl: string;
|
|
118
125
|
readonly #contributions = new KernelPreludeTracker();
|
|
119
126
|
|
|
120
127
|
constructor(options: LocalModuleLoaderOptions) {
|
|
121
128
|
this.#prelude = loaderPrelude(runtimeContext(options));
|
|
129
|
+
this.#cwdUrl = directoryUrl(options.cwd);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
prepareCell(
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
prepareCell(
|
|
133
|
+
code: string,
|
|
134
|
+
contributions: readonly KernelPreludeContribution[] = [],
|
|
135
|
+
sourceFile?: string,
|
|
136
|
+
packageRoot?: string,
|
|
137
|
+
): string {
|
|
138
|
+
// A %load cell resolves its relative imports from its own file's directory for that cell only.
|
|
139
|
+
const cwdUrl = sourceFile === undefined ? this.#cwdUrl : directoryUrl(dirname(sourceFile));
|
|
140
|
+
const base = `\nglobalThis.__senpi_module_context__ = { ...globalThis.__senpi_module_context__, cwdUrl: ${JSON.stringify(cwdUrl)}, packageRootUrl: ${JSON.stringify(packageRoot === undefined ? null : directoryUrl(packageRoot))} };`;
|
|
141
|
+
const prelude = `${this.#prelude}${base}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
|
|
142
|
+
return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code), ...(sourceFile === undefined ? {} : { sourceFile }) })}`;
|
|
127
143
|
}
|
|
128
144
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits text read from the process kernel's fd 1 pipe into the output it carries and the result keys its drain
|
|
3
|
+
* markers name, in order. A read boundary can cut a marker, so a trailing prefix of one is handed back as `carry` for
|
|
4
|
+
* the next read rather than sent as output.
|
|
5
|
+
*
|
|
6
|
+
* Self-contained on purpose: its source also runs inside the pipe reader thread (process-entry.js `DRAINER`).
|
|
7
|
+
*/
|
|
8
|
+
export function scanDrainText(carry, chunk, marker) {
|
|
9
|
+
let text = carry + chunk;
|
|
10
|
+
const parts = [];
|
|
11
|
+
for (;;) {
|
|
12
|
+
const at = text.indexOf(marker);
|
|
13
|
+
if (at === -1) break;
|
|
14
|
+
const close = text.indexOf("\u0000", at + marker.length);
|
|
15
|
+
if (close === -1) break;
|
|
16
|
+
if (at > 0) parts.push({ text: text.slice(0, at) });
|
|
17
|
+
parts.push({ key: text.slice(at + marker.length, close) });
|
|
18
|
+
text = text.slice(close + 1);
|
|
19
|
+
}
|
|
20
|
+
let nextCarry = "";
|
|
21
|
+
const partial = text.lastIndexOf("\u0000");
|
|
22
|
+
if (partial !== -1 && marker.startsWith(text.slice(partial, partial + marker.length))) {
|
|
23
|
+
nextCarry = text.slice(partial);
|
|
24
|
+
text = text.slice(0, partial);
|
|
25
|
+
}
|
|
26
|
+
if (text.length > 0) parts.push({ text });
|
|
27
|
+
return { parts, carry: nextCarry };
|
|
28
|
+
}
|