@code-yeongyu/senpi-codemode 2026.10.9 → 2026.10.10-3
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 +54 -0
- package/README.md +131 -5
- package/package.json +6 -5
- package/src/bridge/protocol.ts +8 -3
- 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/project-resolves.ts +54 -0
- package/src/environments/revision-store.ts +20 -4
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +40 -3
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager.ts +16 -2
- package/src/index.ts +1 -0
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/context-manager.ts +46 -51
- 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 +17 -6
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +462 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/worker-core.js +7 -2
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-runtime.js +1 -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 +8 -0
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/prelude.py +10 -2
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/sandbox/sandbox-cell.ts +116 -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/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 +9 -11
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/load-cell.ts +18 -3
- package/src/tool/magic-cell-host.ts +60 -2
- package/src/tool/magic-cells.ts +12 -2
- package/src/tool/run-eval-cell.ts +30 -3
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +23 -1
|
@@ -2,12 +2,15 @@ import { type ExtensionContext, withBundledBunCommands } from "@code-yeongyu/sen
|
|
|
2
2
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
3
3
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
4
4
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
5
|
+
import { resolveJsIsolation } from "../config/feature-settings.ts";
|
|
6
|
+
import { withoutUntrustedInterpreter } from "../config/project-trust.ts";
|
|
5
7
|
import {
|
|
6
8
|
type CodemodeSettings,
|
|
7
9
|
loadCodemodeSettings,
|
|
8
10
|
type ResolvedCodemodeSettings,
|
|
9
11
|
resolveEnabledLanguages,
|
|
10
12
|
} from "../config/settings.ts";
|
|
13
|
+
import { JsEnvironments } from "../environments/js-environments.ts";
|
|
11
14
|
import { PythonEnvironments } from "../environments/python-environments.ts";
|
|
12
15
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
13
16
|
import {
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
getInterpreterAvailability,
|
|
16
19
|
type InterpreterAvailability,
|
|
17
20
|
} from "../interpreters/detect.ts";
|
|
21
|
+
import { processRuntimeInfo } from "../kernels/js/process-worker.ts";
|
|
18
22
|
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
19
23
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
20
24
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
@@ -49,6 +53,7 @@ export type SessionRuntime = {
|
|
|
49
53
|
readonly executeTool: AgentExecuteTool;
|
|
50
54
|
readonly spawns: boolean;
|
|
51
55
|
readonly pythonEnvironments?: PythonEnvironments;
|
|
56
|
+
readonly jsEnvironments: JsEnvironments;
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
export async function createRuntime(
|
|
@@ -60,12 +65,25 @@ export async function createRuntime(
|
|
|
60
65
|
handles?: HandleRegistry,
|
|
61
66
|
): Promise<SessionRuntime> {
|
|
62
67
|
const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
|
|
68
|
+
const trusted = withoutUntrustedInterpreter(
|
|
69
|
+
loaded.settings,
|
|
70
|
+
loaded.source,
|
|
71
|
+
ctx.cwd,
|
|
72
|
+
typeof ctx.isProjectTrusted === "function" ? () => ctx.isProjectTrusted() : undefined,
|
|
73
|
+
);
|
|
63
74
|
const settings: ResolvedCodemodeSettings = {
|
|
64
|
-
...
|
|
65
|
-
languages: resolveEnabledLanguages(
|
|
75
|
+
...trusted.settings,
|
|
76
|
+
languages: resolveEnabledLanguages(trusted.settings),
|
|
66
77
|
};
|
|
67
78
|
const availability = await getInterpreterAvailability(settings, createInterpreterDetector());
|
|
79
|
+
const pyReason = availability.py.detected.ok ? undefined : availability.py.detected.reason;
|
|
80
|
+
reportSettingsProblems(ctx, [
|
|
81
|
+
...loaded.warnings,
|
|
82
|
+
...(trusted.warning === undefined ? [] : [trusted.warning]),
|
|
83
|
+
...(pyReason === undefined ? [] : [pyReason]),
|
|
84
|
+
]);
|
|
68
85
|
const enabledLanguages = enabledLanguagesFrom(settings, availability);
|
|
86
|
+
const jsProcessIsolation = resolveJsIsolation(settings) === "process";
|
|
69
87
|
const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
|
|
70
88
|
const activeTools = new Set(pi.getActiveTools());
|
|
71
89
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
@@ -93,11 +111,22 @@ export async function createRuntime(
|
|
|
93
111
|
parallelPoolWidth,
|
|
94
112
|
manager,
|
|
95
113
|
enabledLanguages,
|
|
96
|
-
runtimes: runtimesFromAvailability(
|
|
114
|
+
runtimes: runtimesFromAvailability(
|
|
115
|
+
availability,
|
|
116
|
+
(jsProcessIsolation ? processRuntimeInfo(undefined) : undefined) ??
|
|
117
|
+
jsRuntimeInfo(process.versions, process.execPath, jsProcessIsolation),
|
|
118
|
+
),
|
|
97
119
|
settings,
|
|
98
120
|
artifactsDir: artifacts.dir,
|
|
99
121
|
executeTool,
|
|
100
122
|
spawns: activeTools.has(settings.taskTools.task),
|
|
123
|
+
jsEnvironments: new JsEnvironments({
|
|
124
|
+
artifactsDir: artifacts.dir,
|
|
125
|
+
cwd: ctx.cwd,
|
|
126
|
+
runtime: jsRuntimeInfo().name,
|
|
127
|
+
env: { ...process.env, ...sessionEnv },
|
|
128
|
+
settings,
|
|
129
|
+
}),
|
|
101
130
|
...(availability.py.detected.ok && enabledLanguages.py
|
|
102
131
|
? {
|
|
103
132
|
pythonEnvironments: new PythonEnvironments({
|
|
@@ -111,6 +140,14 @@ export async function createRuntime(
|
|
|
111
140
|
};
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/** Settings problems reach the user: a notice with a UI, otherwise stderr (print and RPC runs). */
|
|
144
|
+
function reportSettingsProblems(ctx: ExtensionContext, problems: readonly string[]): void {
|
|
145
|
+
for (const problem of problems) {
|
|
146
|
+
if (ctx.hasUI) ctx.ui.notify(`[senpi-codemode] ${problem}`, "warning");
|
|
147
|
+
else console.error(`[senpi-codemode] ${problem}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
115
152
|
// A cell names tools directly and cannot run tool_search first, so eval opts in to
|
|
116
153
|
// lazy activation. Eligibility still belongs to the extension that registered the tool.
|
|
@@ -10,10 +10,14 @@ export interface JsRuntimeVersions {
|
|
|
10
10
|
export function jsRuntimeInfo(
|
|
11
11
|
versions: JsRuntimeVersions = process.versions,
|
|
12
12
|
execPath: string = process.execPath,
|
|
13
|
+
processIsolation = false,
|
|
13
14
|
): EvalRuntimeInfo {
|
|
14
15
|
const bun = versions.bun;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const base =
|
|
17
|
+
bun !== undefined && bun.length > 0
|
|
18
|
+
? { name: "bun", version: bun, path: execPath }
|
|
19
|
+
: { name: "node", version: versions.node, path: execPath };
|
|
20
|
+
return processIsolation ? { ...base, isolation: "process" } : base;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
|
+
import { readProcessFootprint } from "@code-yeongyu/senpi";
|
|
3
4
|
import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
|
|
4
5
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
5
6
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
7
|
+
import { resolveJsIsolation, resolveKernelToolsEnabled } from "../config/feature-settings.ts";
|
|
6
8
|
import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
|
|
7
9
|
import { collectOrphanedChildren } from "../host-sdk.ts";
|
|
8
10
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
9
11
|
import type { KernelToolsCapability, KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
12
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
10
13
|
import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
|
|
11
14
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
12
15
|
import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
|
|
@@ -252,6 +255,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
252
255
|
this.#options.localRoots ??
|
|
253
256
|
(this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
|
|
254
257
|
if (language === "js") {
|
|
258
|
+
const memoryThresholds = resolveKernelMemoryThresholds(this.#options.settings.memory);
|
|
259
|
+
const processIsolation = resolveJsIsolation(this.#options.settings) === "process";
|
|
255
260
|
const kernel = new JavaScriptKernel({
|
|
256
261
|
sessionId: this.#options.sessionId,
|
|
257
262
|
cwd: this.#options.cwd,
|
|
@@ -259,8 +264,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
259
264
|
onMessage,
|
|
260
265
|
hostToolNames: () => this.#options.listTools?.().map((tool) => tool.name) ?? [],
|
|
261
266
|
foreignLanguageNames: () => this.#foreignKernelToolNames(),
|
|
262
|
-
memory:
|
|
267
|
+
memory: memoryThresholds,
|
|
263
268
|
collectOrphanedChildren,
|
|
269
|
+
...(processIsolation ? { isolation: "process" as const } : {}),
|
|
270
|
+
...(processIsolation && memoryThresholds !== undefined
|
|
271
|
+
? { processMemory: { thresholds: memoryThresholds, readFootprint: readProcessFootprint } }
|
|
272
|
+
: {}),
|
|
273
|
+
...(resolveKernelToolsEnabled(this.#options.settings) ? {} : { kernelToolsEnabled: false }),
|
|
264
274
|
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
265
275
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
266
276
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
@@ -276,10 +286,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
276
286
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
277
287
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
278
288
|
};
|
|
289
|
+
// The Python prelude reads this when a cell defines an @tool; only a disabled setting adds it.
|
|
290
|
+
const sessionEnv: SessionEnvironment | undefined = resolveKernelToolsEnabled(this.#options.settings)
|
|
291
|
+
? this.#options.sessionEnv
|
|
292
|
+
: { ...this.#options.sessionEnv, SENPI_CODEMODE_KERNEL_TOOLS: "0" };
|
|
279
293
|
const shared = {
|
|
280
294
|
sessionId: this.#options.sessionId,
|
|
281
295
|
cwd: this.#options.cwd,
|
|
282
|
-
...(
|
|
296
|
+
...(sessionEnv ? { sessionEnv } : {}),
|
|
283
297
|
connection,
|
|
284
298
|
onMessage,
|
|
285
299
|
...lifecycle,
|
package/src/index.ts
CHANGED
|
@@ -158,6 +158,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
158
158
|
settings: runtime.settings,
|
|
159
159
|
artifactsDir: runtime.artifactsDir,
|
|
160
160
|
...(runtime.pythonEnvironments === undefined ? {} : { pythonEnvironments: runtime.pythonEnvironments }),
|
|
161
|
+
jsEnvironments: runtime.jsEnvironments,
|
|
161
162
|
cellManager,
|
|
162
163
|
handles,
|
|
163
164
|
executionTracker: manager,
|
|
@@ -20,6 +20,8 @@ export interface InterpreterDetected {
|
|
|
20
20
|
|
|
21
21
|
export interface InterpreterUnavailable {
|
|
22
22
|
readonly ok: false;
|
|
23
|
+
/** Why a configured interpreter could not be used; unset when detection simply found none. */
|
|
24
|
+
readonly reason?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type InterpreterDetection = InterpreterDetected | InterpreterUnavailable;
|
|
@@ -43,6 +45,8 @@ export interface CreateInterpreterDetectorOptions {
|
|
|
43
45
|
|
|
44
46
|
export interface InterpreterDetector {
|
|
45
47
|
detect(language: CodemodeLanguage): Promise<InterpreterDetection>;
|
|
48
|
+
/** Probes exactly this executable (no PATH candidates, no splitting on spaces). */
|
|
49
|
+
detectExplicit(path: string): Promise<InterpreterDetection>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface LanguageAvailability {
|
|
@@ -74,6 +78,9 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
|
|
|
74
78
|
cache.set(language, pending);
|
|
75
79
|
return pending;
|
|
76
80
|
},
|
|
81
|
+
detectExplicit(path) {
|
|
82
|
+
return probeExplicit(path, probe);
|
|
83
|
+
},
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -82,7 +89,7 @@ export async function getInterpreterAvailability(
|
|
|
82
89
|
detector: InterpreterDetector,
|
|
83
90
|
): Promise<InterpreterAvailability> {
|
|
84
91
|
return {
|
|
85
|
-
py: await
|
|
92
|
+
py: await pythonAvailability(settings, detector),
|
|
86
93
|
js: await availabilityFor("js", settings.languages.js, detector),
|
|
87
94
|
rb: await availabilityFor("rb", settings.languages.rb, detector),
|
|
88
95
|
jl: await availabilityFor("jl", settings.languages.jl, detector),
|
|
@@ -100,6 +107,32 @@ async function availabilityFor(
|
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** `languages.pyInterpreter`, when set, is the only interpreter Python uses; a path that does not answer makes Python unavailable. */
|
|
111
|
+
async function pythonAvailability(
|
|
112
|
+
settings: CodemodeSettings,
|
|
113
|
+
detector: InterpreterDetector,
|
|
114
|
+
): Promise<LanguageAvailability> {
|
|
115
|
+
const configured = settings.languages.pyInterpreter;
|
|
116
|
+
if (configured === undefined || !settings.languages.py)
|
|
117
|
+
return await availabilityFor("py", settings.languages.py, detector);
|
|
118
|
+
return { enabled: true, detected: await detector.detectExplicit(configured) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function probeExplicit(path: string, probe: ExecFileProbe): Promise<InterpreterDetection> {
|
|
122
|
+
const reason = (detail: string): InterpreterUnavailable => ({
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `languages.pyInterpreter "${path}" ${detail}; Python is unavailable in this session`,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
const result = await probe(path, ["--version"], { timeoutMs: probeTimeoutMs });
|
|
128
|
+
const version = parseVersion(`${result.stdout}\n${result.stderr}`);
|
|
129
|
+
if (version === null) return reason("did not report a Python version");
|
|
130
|
+
return { ok: true, path, version, resolvedPath: path };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return reason(`could not run (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
103
136
|
async function detectUncached(
|
|
104
137
|
language: CodemodeLanguage,
|
|
105
138
|
hostPlatform: NodeJS.Platform,
|
package/src/kernels/AGENTS.md
CHANGED
|
@@ -16,6 +16,8 @@ runner/prelude assets).
|
|
|
16
16
|
| Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
|
|
17
17
|
| Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
|
|
18
18
|
| Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
|
|
19
|
+
| JS process isolation | `js/process-worker.ts` (host side), `js/process-entry.js` (child entry), `js/kernel-memory.ts`, `js/control-frames.ts` |
|
|
20
|
+
| Sandbox cells | `sandbox/sandbox-cell.ts` (host executor); `sandbox/vendor/pi-codemode/**` (vendored runtime, see its `VENDORED.md`) |
|
|
19
21
|
|
|
20
22
|
## CONVENTIONS
|
|
21
23
|
|
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
if ("
|
|
201
|
-
this.#
|
|
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
|
|
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.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.#runs.
|
|
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
|
+
}
|
|
@@ -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. */
|