@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.
- package/CHANGELOG.md +190 -0
- package/README.md +161 -11
- package/package.json +6 -5
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +9 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +52 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/editable-check.ts +1 -1
- package/src/environments/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 +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/environments/revision-store.ts +20 -4
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +51 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +20 -6
- package/src/index.ts +3 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/runner.jl +11 -22
- 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 +46 -51
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/inline-worker.ts +2 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +25 -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 +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-tree-host.ts +4 -8
- package/src/kernels/js/process-tree.d.ts +11 -0
- package/src/kernels/js/process-tree.js +55 -10
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/windows-tree-kill-host.ts +117 -0
- package/src/kernels/js/windows-tree-kill.d.ts +19 -0
- package/src/kernels/js/windows-tree-kill.js +100 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +46 -12
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-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 +66 -13
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +9 -0
- package/src/kernels/js/worker-slot.ts +8 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/prelude.py +69 -27
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- 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/bun-ffi.ts +24 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/kernel-memory-host.ts +30 -7
- package/src/kernels/shared/process-cpu.ts +7 -3
- package/src/kernels/shared/process-group-cpu.ts +6 -3
- package/src/kernels/shared/subprocess-kernel.ts +29 -3
- package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
- package/src/kernels/shared/subprocess-process.ts +24 -2
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +12 -11
- 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 +3 -0
- package/src/tool/eval-tool.ts +19 -5
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- 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/managed-cell.ts +0 -4
- package/src/tool/render-agent.ts +110 -0
- package/src/tool/render-blocks.ts +252 -0
- package/src/tool/render-call.ts +130 -0
- package/src/tool/render-cell.ts +165 -0
- package/src/tool/render-detail.ts +152 -0
- package/src/tool/render-live.ts +288 -0
- package/src/tool/render-result.ts +202 -0
- package/src/tool/render-status.ts +166 -0
- package/src/tool/render-tail.ts +139 -0
- package/src/tool/render.ts +3 -1069
- package/src/tool/run-eval-cell.ts +53 -16
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +34 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// senpi#2995: Bun.$, node:child_process exec/execFile and the synchronous spawners leave their children in the
|
|
2
|
+
// agent's process group (Bun.$ has no such option, Bun ignores `detached` for exec/execFile, and a synchronous call
|
|
3
|
+
// keeps the terminal), so a group-wide signal from the same cell stops the agent. Such a command gets a notice; the
|
|
4
|
+
// notice only warns and never fails the cell.
|
|
5
|
+
|
|
6
|
+
// A command starts a (possibly indented) line, follows `;`, `|`, `&`, `(`, a backtick, `{`, `!`, a shell keyword
|
|
7
|
+
// (`then`, `do`, `else`) or a runner (`nohup`, `time`, `xargs`, `env`, `exec`, `sudo` with flags), or opens a `-c '...'` script; a
|
|
8
|
+
// `kill` word anywhere else (`git log --grep kill`, `echo kill ...`) is text.
|
|
9
|
+
const COMMAND_POSITION = String.raw`(?:^\s*|[;|&(\x60]\s*|\b(?:then|do|else|nohup|time|xargs|env|exec)\s+|[{!]\s+|\bsudo(?:\s+-\S+)*\s+|\s-[A-Za-z]*c\s+['"]\s*)`;
|
|
10
|
+
|
|
11
|
+
// A group target is a dash argument after the signal options: a number, `$var`, `${var}` or `$(...)`.
|
|
12
|
+
const GROUP_TARGET = String.raw`-(?:\d+|\$\$|\$\{?\w+\}?|\$\()`;
|
|
13
|
+
const SIGNAL_OPTION = String.raw`(?:-s\s+\w+|-n\s+\d+|-[A-Za-z]+\d*|-\d+)`;
|
|
14
|
+
// The first dash argument is a signal, so a group is one after `--`, after a signal option, or after a plain pid:
|
|
15
|
+
// `kill -- -<pgid>`, `kill -SIG -<pgid>`, `kill -TERM 1234 -5678`, `kill -- -$$`.
|
|
16
|
+
const PID = String.raw`[^\s\-;|&][^\s;|&]*`;
|
|
17
|
+
const KILL_GROUP = new RegExp(
|
|
18
|
+
String.raw`${COMMAND_POSITION}kill\b(?:` +
|
|
19
|
+
String.raw`\s+--\s+${GROUP_TARGET}` +
|
|
20
|
+
String.raw`|(?:\s+${SIGNAL_OPTION})+(?:\s+${PID})*\s+(?:--\s+)?${GROUP_TARGET}` +
|
|
21
|
+
String.raw`|(?:\s+${SIGNAL_OPTION})*(?:\s+${PID})+\s+${GROUP_TARGET})`,
|
|
22
|
+
"mu",
|
|
23
|
+
);
|
|
24
|
+
const PKILL_GROUP = new RegExp(String.raw`${COMMAND_POSITION}pkill\b[^\n;|&]*\s(?:-g\d*|--pgroup)\b`, "mu");
|
|
25
|
+
// `killall` in command position (also after `sudo` or as a `-c` script) signals every process with that name, which
|
|
26
|
+
// can include the agent's own (bun, node, senpi). Inside an `echo` string it is only text.
|
|
27
|
+
const KILLALL = new RegExp(String.raw`${COMMAND_POSITION}killall\s`, "mu");
|
|
28
|
+
|
|
29
|
+
export const GROUP_SIGNAL_NOTICE_TAG = "[senpi:group-signal]";
|
|
30
|
+
|
|
31
|
+
export function signalsProcessGroup(commandText) {
|
|
32
|
+
return KILL_GROUP.test(commandText) || PKILL_GROUP.test(commandText) || KILLALL.test(commandText);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function groupSignalNotice(api, readGroup = agentProcessGroup) {
|
|
36
|
+
const pgid = readGroup();
|
|
37
|
+
const group = pgid === undefined ? "the agent's process group" : `the agent's process group (${pgid})`;
|
|
38
|
+
return (
|
|
39
|
+
`${GROUP_SIGNAL_NOTICE_TAG} This command signals a process group. Children of ${api} share ${group}, so ` +
|
|
40
|
+
"signalling that group can stop this session. Start background jobs with Bun.spawn or child_process.spawn, " +
|
|
41
|
+
"which give them their own group, or signal them by pid.\n"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cachedGroup;
|
|
46
|
+
|
|
47
|
+
// A bounded `ps` lookup, kept once it succeeds; a missing `ps` or a failed run only drops the number from this notice.
|
|
48
|
+
export function agentProcessGroup(spawnSync = globalThis.Bun?.spawnSync) {
|
|
49
|
+
if (cachedGroup !== undefined) return cachedGroup;
|
|
50
|
+
let text = "";
|
|
51
|
+
try {
|
|
52
|
+
const result = spawnSync?.(["ps", "-o", "pgid=", "-p", String(process.pid)], { timeout: 2_000 });
|
|
53
|
+
text = result?.success ? String(result.stdout).trim() : "";
|
|
54
|
+
} catch {
|
|
55
|
+
text = "";
|
|
56
|
+
}
|
|
57
|
+
if (!/^\d+$/u.test(text)) return undefined;
|
|
58
|
+
cachedGroup = text;
|
|
59
|
+
return cachedGroup;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shellCommandText(strings, expressions) {
|
|
63
|
+
if (!Array.isArray(strings)) return String(strings ?? "");
|
|
64
|
+
return strings.reduce((text, part, index) => text + part + (index < expressions.length ? String(expressions[index]) : ""), "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { promisify } = process.getBuiltinModule("node:util");
|
|
68
|
+
const SCANNED_SPAWNERS = ["exec", "execFile", "execSync", "execFileSync", "spawnSync"];
|
|
69
|
+
|
|
70
|
+
function commandTextOf(name, args) {
|
|
71
|
+
if (name === "exec" || name === "execSync") return String(args[0]);
|
|
72
|
+
return [args[0], ...(Array.isArray(args[1]) ? args[1] : [])].join(" ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function noticeChildProcessGroupSignals(emitText, isActive) {
|
|
76
|
+
const childProcess = process.getBuiltinModule("node:child_process");
|
|
77
|
+
const originals = new Map();
|
|
78
|
+
for (const name of SCANNED_SPAWNERS) {
|
|
79
|
+
const original = childProcess[name];
|
|
80
|
+
if (typeof original !== "function") continue;
|
|
81
|
+
originals.set(name, original);
|
|
82
|
+
const scan = (args) => {
|
|
83
|
+
try {
|
|
84
|
+
if (isActive() && signalsProcessGroup(commandTextOf(name, args))) {
|
|
85
|
+
emitText("stderr", groupSignalNotice(`child_process.${name}`));
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// The notice is advisory; the call itself always runs.
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const scanned = function scannedSpawner(...args) {
|
|
92
|
+
scan(args);
|
|
93
|
+
return original.apply(this, args);
|
|
94
|
+
};
|
|
95
|
+
// Every own property, symbols included: `util.promisify.custom` is what makes promisify(exec) resolve to
|
|
96
|
+
// { stdout, stderr }. That custom function calls the original directly, so it is scanned too.
|
|
97
|
+
for (const key of Reflect.ownKeys(original)) {
|
|
98
|
+
if (key === "length" || key === "name" || key === "prototype" || key === promisify.custom) continue;
|
|
99
|
+
const descriptor = Object.getOwnPropertyDescriptor(original, key);
|
|
100
|
+
if (descriptor) Object.defineProperty(scanned, key, descriptor);
|
|
101
|
+
}
|
|
102
|
+
const customPromisify = original[promisify.custom];
|
|
103
|
+
if (typeof customPromisify === "function") {
|
|
104
|
+
Object.defineProperty(scanned, promisify.custom, {
|
|
105
|
+
configurable: true,
|
|
106
|
+
value: function scannedPromisified(...args) {
|
|
107
|
+
scan(args);
|
|
108
|
+
return customPromisify.apply(this, args);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
childProcess[name] = scanned;
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
for (const [name, original] of originals) childProcess[name] = original;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,8 @@ export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntry
|
|
|
19
19
|
|
|
20
20
|
export interface WorkerLike {
|
|
21
21
|
readonly mode: JavaScriptKernelMode;
|
|
22
|
+
/** The child pid when `mode` is `"process"`; the slot reports it for orphan accounting after a crash. */
|
|
23
|
+
readonly pid?: number;
|
|
22
24
|
postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void;
|
|
23
25
|
onMessage(handler: (message: KernelToHostMessage) => void): () => void;
|
|
24
26
|
onError(handler: (error: Error) => void): () => void;
|
|
@@ -7,7 +7,7 @@ import type { JavaScriptInterruptBounds } from "./interrupt-bounds.ts";
|
|
|
7
7
|
export type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
|
|
8
8
|
export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
9
9
|
|
|
10
|
-
export type JavaScriptKernelMode = "worker" | "inline";
|
|
10
|
+
export type JavaScriptKernelMode = "worker" | "inline" | "process";
|
|
11
11
|
|
|
12
12
|
/** Snapshot or live provider consulted when the worker registry checks collisions. */
|
|
13
13
|
export type KernelToolNameSource = readonly string[] | (() => readonly string[]);
|
|
@@ -30,8 +30,23 @@ export interface JavaScriptKernelOptions {
|
|
|
30
30
|
readonly workerEntryUrl?: URL;
|
|
31
31
|
/** Per-session PI_* values applied to the worker environment before the first cell runs. */
|
|
32
32
|
readonly sessionEnv?: SessionEnvironment;
|
|
33
|
+
/** Opt-in child-process kernel; absent keeps the worker-thread kernel. */
|
|
34
|
+
readonly isolation?: "process";
|
|
35
|
+
/** Resolution root for the process-mode runtime (bun, then node); `""` names a machine with none on PATH. */
|
|
36
|
+
readonly processCommandPath?: string;
|
|
37
|
+
/** The host's executable, which runs the child when it is bun or node; defaults to `process.execPath`. */
|
|
38
|
+
readonly processExecPath?: string;
|
|
39
|
+
/** How long a process-mode child gets to report ready; defaults to `PROCESS_STARTUP_DEADLINE_MS`. */
|
|
40
|
+
readonly processStartupDeadlineMs?: number;
|
|
41
|
+
/** Process-mode memory policy: the host reads the child's footprint instead of the in-heap worker reading. */
|
|
42
|
+
readonly processMemory?: {
|
|
43
|
+
readonly thresholds: KernelMemoryThresholds;
|
|
44
|
+
readonly readFootprint?: (pid: number) => { readonly bytes: number } | undefined;
|
|
45
|
+
};
|
|
33
46
|
/** Host tool names denied as JS kernel-tool identifiers (init protocol). */
|
|
34
47
|
readonly hostToolNames?: KernelToolNameSource;
|
|
48
|
+
/** `kernelTools.enabled`; false makes `tool(fn)` refuse with `tools_unavailable`. Unset keeps them on. */
|
|
49
|
+
readonly kernelToolsEnabled?: boolean;
|
|
35
50
|
/** Tool names registered in another kernel language, denied as JS kernel-tool identifiers. */
|
|
36
51
|
readonly foreignLanguageNames?: KernelToolNameSource;
|
|
37
52
|
/** Post-cell collection, notice, and ceiling thresholds; absent leaves the worker's memory unmanaged. */
|
|
@@ -6,7 +6,7 @@ import type { ResultMessage } from "./kernel-contract.ts";
|
|
|
6
6
|
|
|
7
7
|
export interface JavaScriptMemoryReading {
|
|
8
8
|
readonly liveBytes: number;
|
|
9
|
-
readonly measure: "heap";
|
|
9
|
+
readonly measure: "heap" | "footprint";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** A query waits at most this long: the worker answers between cells, never inside one. */
|
|
@@ -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);
|
|
@@ -79,6 +79,14 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
79
79
|
" const specifier = String(source);",
|
|
80
80
|
" const kernelBun = globalThis[Symbol.for('senpi.kernel.bun')];",
|
|
81
81
|
" if (specifier === 'bun' && kernelBun) return { ...(await import('bun')), WebView: kernelBun.WebView, default: kernelBun };",
|
|
82
|
+
// Under Bun a cell's named import (`import { spawn } from 'node:child_process'`) binds the builtin's original
|
|
83
|
+
// export even after the worker patched the module object (worker-cwd.js), so the session cwd and the cell
|
|
84
|
+
// children's own process group (senpi#2995) were skipped. The patched module objects are served instead.
|
|
85
|
+
" const patched = /^(?:node:)?(child_process|fs|fs\\/promises|path)$/.exec(specifier);",
|
|
86
|
+
" if (patched && typeof process.getBuiltinModule === 'function') {",
|
|
87
|
+
" const builtin = process.getBuiltinModule('node:' + patched[1]);",
|
|
88
|
+
" if (builtin) return { ...builtin, default: builtin };",
|
|
89
|
+
" }",
|
|
82
90
|
" const match = /^([a-z][a-z0-9+.-]*):\\/\\/(.*)$/i.exec(specifier);",
|
|
83
91
|
" let target = specifier;",
|
|
84
92
|
" if (match) {",
|
|
@@ -98,7 +106,13 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
98
106
|
" const urlModule = await import('node:url');",
|
|
99
107
|
" target = urlModule.pathToFileURL(specifier).href;",
|
|
100
108
|
" }",
|
|
101
|
-
"
|
|
109
|
+
" const load = (resolved) => (options === undefined ? import(resolved) : import(resolved, options));",
|
|
110
|
+
" const resolvePackage = globalThis[Symbol.for('senpi.kernel.resolvePackage')];",
|
|
111
|
+
" if (target === specifier && typeof resolvePackage === 'function') {",
|
|
112
|
+
" const resolved = resolvePackage(specifier, [context.cwdUrl, context.packageRootUrl]);",
|
|
113
|
+
" if (resolved !== undefined) return await load(resolved);",
|
|
114
|
+
" }",
|
|
115
|
+
" return await load(target);",
|
|
102
116
|
"};",
|
|
103
117
|
].join("\n");
|
|
104
118
|
}
|
|
@@ -115,18 +129,23 @@ function contributionPrelude(plan: KernelPreludePlan): string {
|
|
|
115
129
|
|
|
116
130
|
export class LocalModuleLoader {
|
|
117
131
|
readonly #prelude: string;
|
|
132
|
+
readonly #cwdUrl: string;
|
|
118
133
|
readonly #contributions = new KernelPreludeTracker();
|
|
119
134
|
|
|
120
135
|
constructor(options: LocalModuleLoaderOptions) {
|
|
121
136
|
this.#prelude = loaderPrelude(runtimeContext(options));
|
|
137
|
+
this.#cwdUrl = directoryUrl(options.cwd);
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
prepareCell(
|
|
140
|
+
prepareCell(
|
|
141
|
+
code: string,
|
|
142
|
+
contributions: readonly KernelPreludeContribution[] = [],
|
|
143
|
+
sourceFile?: string,
|
|
144
|
+
packageRoot?: string,
|
|
145
|
+
): string {
|
|
125
146
|
// A %load cell resolves its relative imports from its own file's directory for that cell only.
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
? ""
|
|
129
|
-
: `\nglobalThis.__senpi_module_context__ = { ...globalThis.__senpi_module_context__, cwdUrl: ${JSON.stringify(directoryUrl(dirname(sourceFile)))} };`;
|
|
147
|
+
const cwdUrl = sourceFile === undefined ? this.#cwdUrl : directoryUrl(dirname(sourceFile));
|
|
148
|
+
const base = `\nglobalThis.__senpi_module_context__ = { ...globalThis.__senpi_module_context__, cwdUrl: ${JSON.stringify(cwdUrl)}, packageRootUrl: ${JSON.stringify(packageRoot === undefined ? null : directoryUrl(packageRoot))} };`;
|
|
130
149
|
const prelude = `${this.#prelude}${base}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
|
|
131
150
|
return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code), ...(sourceFile === undefined ? {} : { sourceFile }) })}`;
|
|
132
151
|
}
|
|
@@ -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
|
+
}
|