@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
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* Windows has no process groups; this is the interpreter itself when it is launched directly.
|
|
9
9
|
* - anything else, or Windows under Node: `undefined`, and the caller treats the CPU as unknown.
|
|
10
10
|
*/
|
|
11
|
+
import { type BunFfi, loadBunFfi } from "./bun-ffi.ts";
|
|
11
12
|
import { readProcessGroupCpuTime } from "./process-group-cpu.ts";
|
|
12
13
|
|
|
13
|
-
type BunFfi = typeof import("bun:ffi");
|
|
14
14
|
type CpuReader = (pid: number) => bigint | undefined;
|
|
15
15
|
|
|
16
16
|
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
@@ -26,7 +26,7 @@ export function readKernelCpuTime(pid: number): bigint | undefined {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function createWindowsReader(): CpuReader | undefined {
|
|
29
|
-
const ffi =
|
|
29
|
+
const ffi = loadBunFfi();
|
|
30
30
|
if (ffi === undefined) return undefined;
|
|
31
31
|
try {
|
|
32
32
|
return bindGetProcessTimes(ffi);
|
|
@@ -37,7 +37,11 @@ function createWindowsReader(): CpuReader | undefined {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function bindGetProcessTimes({ dlopen, FFIType, ptr }: BunFfi): CpuReader {
|
|
40
|
-
const library = dlopen
|
|
40
|
+
const library = dlopen<{
|
|
41
|
+
OpenProcess: (access: number, inheritHandle: number, pid: number) => number | null;
|
|
42
|
+
GetProcessTimes: (handle: number, create: number, exit: number, kernel: number, user: number) => number;
|
|
43
|
+
CloseHandle: (handle: number) => number;
|
|
44
|
+
}>("kernel32.dll", {
|
|
41
45
|
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.ptr },
|
|
42
46
|
GetProcessTimes: {
|
|
43
47
|
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
17
|
import { readdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { type BunFfi, loadBunFfi } from "./bun-ffi.ts";
|
|
18
19
|
|
|
19
|
-
type BunFfi = typeof import("bun:ffi");
|
|
20
20
|
type GroupCpuReader = (pgid: number) => bigint | undefined;
|
|
21
21
|
|
|
22
22
|
const RUSAGE_INFO_V2 = 2;
|
|
@@ -59,7 +59,7 @@ export function parsePsCpuTime(text: string): bigint | undefined {
|
|
|
59
59
|
function createGroupReader(): GroupCpuReader | undefined {
|
|
60
60
|
if (process.platform === "linux") return linuxGroupReader;
|
|
61
61
|
if (process.platform !== "darwin") return undefined;
|
|
62
|
-
const ffi =
|
|
62
|
+
const ffi = loadBunFfi();
|
|
63
63
|
if (ffi === undefined) return darwinPsGroupReader;
|
|
64
64
|
try {
|
|
65
65
|
return darwinGroupReader(ffi);
|
|
@@ -116,7 +116,10 @@ function linuxGroupReader(pgid: number): bigint | undefined {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function darwinGroupReader({ dlopen, FFIType, ptr }: BunFfi): GroupCpuReader {
|
|
119
|
-
const library = dlopen
|
|
119
|
+
const library = dlopen<{
|
|
120
|
+
proc_listpgrppids: (pgid: number, buffer: number, byteLength: number) => number;
|
|
121
|
+
proc_pid_rusage: (pid: number, flavor: number, buffer: number) => number;
|
|
122
|
+
}>("libSystem.B.dylib", {
|
|
120
123
|
proc_listpgrppids: { args: [FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
121
124
|
proc_pid_rusage: { args: [FFIType.i32, FFIType.i32, FFIType.ptr], returns: FFIType.i32 },
|
|
122
125
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// allow: SIZE_OK — subprocess protocol, FIFO ownership, retirement, and replacement form one serialized state machine.
|
|
1
2
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
3
|
import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
4
|
import type { KernelInterruptHandle, PendingCell } from "../../tool/types.ts";
|
|
@@ -7,6 +8,7 @@ import { applySessionEnvironment } from "../session-env.ts";
|
|
|
7
8
|
import { describeExit } from "./kernel-death.ts";
|
|
8
9
|
import { KernelMemoryHost } from "./kernel-memory-host.ts";
|
|
9
10
|
import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
|
|
11
|
+
import { SubprocessMemoryGlobals } from "./subprocess-memory-globals.ts";
|
|
10
12
|
import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
|
|
11
13
|
import { SubprocessRunQueue } from "./subprocess-queue.ts";
|
|
12
14
|
import {
|
|
@@ -39,6 +41,7 @@ export class SubprocessKernel {
|
|
|
39
41
|
private readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
40
42
|
private readonly runs = new SubprocessRunQueue();
|
|
41
43
|
private readonly memory: KernelMemoryHost | null;
|
|
44
|
+
private readonly globals = new SubprocessMemoryGlobals();
|
|
42
45
|
private process: SubprocessProcess | null = null;
|
|
43
46
|
private processReady = false;
|
|
44
47
|
private startup: SubprocessStartupWatchdog | null = null;
|
|
@@ -90,8 +93,10 @@ export class SubprocessKernel {
|
|
|
90
93
|
this.runs.settleAll(new CellInterruptedError(reason));
|
|
91
94
|
return { stateRetained: Promise.resolve(true) };
|
|
92
95
|
}
|
|
96
|
+
if (this.globals.completeWithoutGlobals()) return { stateRetained: Promise.resolve(true) };
|
|
93
97
|
const process = this.process;
|
|
94
98
|
process?.retire();
|
|
99
|
+
this.globals.reset();
|
|
95
100
|
this.runs.clearToolCalls();
|
|
96
101
|
const run = this.runs.active;
|
|
97
102
|
if (!run) return { stateRetained: Promise.resolve(true) };
|
|
@@ -126,6 +131,7 @@ export class SubprocessKernel {
|
|
|
126
131
|
|
|
127
132
|
async reset(): Promise<void> {
|
|
128
133
|
if (this.closed) throw new KernelClosedError();
|
|
134
|
+
this.globals.reset();
|
|
129
135
|
this.settleAll(new KernelResetError());
|
|
130
136
|
this.runs.clearToolCalls();
|
|
131
137
|
await this.restartProcess(this.process);
|
|
@@ -135,6 +141,7 @@ export class SubprocessKernel {
|
|
|
135
141
|
async close(): Promise<void> {
|
|
136
142
|
const wasClosed = this.closed;
|
|
137
143
|
this.closed = true;
|
|
144
|
+
this.globals.reset();
|
|
138
145
|
this.startup?.stop();
|
|
139
146
|
if (!wasClosed) {
|
|
140
147
|
this.settleAll(new KernelClosingError());
|
|
@@ -175,6 +182,7 @@ export class SubprocessKernel {
|
|
|
175
182
|
if (this.runs.active !== run || run.settled) return;
|
|
176
183
|
const process = this.process;
|
|
177
184
|
process?.retire();
|
|
185
|
+
this.globals.reset();
|
|
178
186
|
this.runs.clearToolCalls();
|
|
179
187
|
this.runs.releaseActive(run);
|
|
180
188
|
this.runs.settle(run, timeoutResult(run, timeoutMs));
|
|
@@ -199,6 +207,7 @@ export class SubprocessKernel {
|
|
|
199
207
|
},
|
|
200
208
|
});
|
|
201
209
|
this.process = process;
|
|
210
|
+
this.globals.reset();
|
|
202
211
|
this.processReady = false;
|
|
203
212
|
this.startup?.stop();
|
|
204
213
|
const startup = this.options.startup;
|
|
@@ -237,6 +246,7 @@ export class SubprocessKernel {
|
|
|
237
246
|
if (!this.accepts(process)) return;
|
|
238
247
|
if (!this.processReady) this.startup?.observe(message);
|
|
239
248
|
if (message.type === "ready") {
|
|
249
|
+
this.globals.reset(message.memoryGlobals === true);
|
|
240
250
|
this.startup?.stop();
|
|
241
251
|
this.processReady = true;
|
|
242
252
|
this.runs.handleMessage(message, this.onMessage);
|
|
@@ -248,9 +258,23 @@ export class SubprocessKernel {
|
|
|
248
258
|
this.failClosed(new KernelStartupError(message.error.message));
|
|
249
259
|
return;
|
|
250
260
|
}
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
const run = this.runs.active;
|
|
262
|
+
if (message.type === "memory-globals-result") {
|
|
263
|
+
if (this.memory) this.globals.reply({ process, run }, message, this.memory);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (message.type === "result" && run?.input.cellId === message.cellId && this.memory) {
|
|
267
|
+
this.globals.request({ process, run, result: message }, this.memory, (result) => {
|
|
268
|
+
if (!this.accepts(process) || this.runs.active !== run || run.settled) return;
|
|
269
|
+
this.settleMessage(result);
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.settleMessage(message);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private settleMessage(message: KernelToHostMessage): void {
|
|
277
|
+
if (!this.runs.handleMessage(message, this.onMessage)) return;
|
|
254
278
|
this.pumpRuns();
|
|
255
279
|
this.recycleOverCeilingWhenIdle();
|
|
256
280
|
}
|
|
@@ -270,6 +294,7 @@ export class SubprocessKernel {
|
|
|
270
294
|
return;
|
|
271
295
|
}
|
|
272
296
|
this.process = null;
|
|
297
|
+
this.globals.reset();
|
|
273
298
|
this.processReady = false;
|
|
274
299
|
const error = new KernelExitedError(signal ?? code ?? "unknown");
|
|
275
300
|
if (this.options.onDeath) this.die(error, describeExit(code, signal), this.options.onDeath);
|
|
@@ -362,6 +387,7 @@ export class SubprocessKernel {
|
|
|
362
387
|
|
|
363
388
|
private failClosed(error: Error): void {
|
|
364
389
|
this.startup?.stop();
|
|
390
|
+
this.globals.reset();
|
|
365
391
|
const process = this.process;
|
|
366
392
|
this.failure = error;
|
|
367
393
|
this.closed = true;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { KernelMemoryReport } from "../../bridge/memory-protocol.ts";
|
|
2
|
+
import { encodeBridgeFrame, type KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
+
import type { KernelMemoryHost } from "./kernel-memory-host.ts";
|
|
4
|
+
import type { KernelResult } from "./subprocess-contract.ts";
|
|
5
|
+
import type { SubprocessProcess } from "./subprocess-process.ts";
|
|
6
|
+
import type { PendingRun } from "./subprocess-run.ts";
|
|
7
|
+
|
|
8
|
+
const GLOBALS_TIMEOUT_MS = 5_000;
|
|
9
|
+
type GlobalsProcess = Pick<SubprocessProcess, "send"> & {
|
|
10
|
+
readonly child: { readonly pid?: number };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface PendingGlobals {
|
|
14
|
+
readonly process: GlobalsProcess;
|
|
15
|
+
readonly run: PendingRun;
|
|
16
|
+
readonly result: KernelResult;
|
|
17
|
+
readonly report: KernelMemoryReport;
|
|
18
|
+
readonly memory: KernelMemoryHost;
|
|
19
|
+
readonly finish: (result: KernelResult) => void;
|
|
20
|
+
readonly timer: ReturnType<typeof setTimeout>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Holds FIFO cell ownership until its bounded diagnostic is returned or its deadline expires. */
|
|
24
|
+
export class SubprocessMemoryGlobals {
|
|
25
|
+
#pending: PendingGlobals | null = null;
|
|
26
|
+
#available = false;
|
|
27
|
+
|
|
28
|
+
reset(available = false): void {
|
|
29
|
+
if (this.#pending !== null) clearTimeout(this.#pending.timer);
|
|
30
|
+
this.#pending = null;
|
|
31
|
+
this.#available = available;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
request(
|
|
35
|
+
owner: {
|
|
36
|
+
readonly process: GlobalsProcess;
|
|
37
|
+
readonly run: PendingRun;
|
|
38
|
+
readonly result: KernelResult;
|
|
39
|
+
},
|
|
40
|
+
memory: KernelMemoryHost,
|
|
41
|
+
finish: (result: KernelResult) => void,
|
|
42
|
+
): void {
|
|
43
|
+
if (this.#pending !== null) return;
|
|
44
|
+
const report = memory.readReport(owner.result, owner.process.child.pid);
|
|
45
|
+
if (!this.#available || report === undefined || !memory.needsGlobals(report)) {
|
|
46
|
+
finish(memory.annotateReport(owner.result, report));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const pending: PendingGlobals = {
|
|
50
|
+
...owner,
|
|
51
|
+
report,
|
|
52
|
+
memory,
|
|
53
|
+
finish,
|
|
54
|
+
timer: setTimeout(() => {
|
|
55
|
+
if (this.#pending !== pending) return;
|
|
56
|
+
this.#pending = null;
|
|
57
|
+
pending.finish(memory.annotateReport(pending.result, report));
|
|
58
|
+
}, GLOBALS_TIMEOUT_MS),
|
|
59
|
+
};
|
|
60
|
+
this.#pending = pending;
|
|
61
|
+
owner.process.send(
|
|
62
|
+
encodeBridgeFrame({
|
|
63
|
+
type: "memory-globals",
|
|
64
|
+
cellId: owner.result.cellId,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Settles an already-finished cell immediately when stop arrives during its optional diagnostic. */
|
|
70
|
+
completeWithoutGlobals(): boolean {
|
|
71
|
+
const pending = this.#pending;
|
|
72
|
+
if (pending === null) return false;
|
|
73
|
+
clearTimeout(pending.timer);
|
|
74
|
+
this.#pending = null;
|
|
75
|
+
pending.finish(pending.memory.annotateReport(pending.result, pending.report));
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
reply(
|
|
80
|
+
owner: {
|
|
81
|
+
readonly process: GlobalsProcess;
|
|
82
|
+
readonly run: PendingRun | null;
|
|
83
|
+
},
|
|
84
|
+
message: Extract<KernelToHostMessage, { type: "memory-globals-result" }>,
|
|
85
|
+
memory: KernelMemoryHost,
|
|
86
|
+
): void {
|
|
87
|
+
const pending = this.#pending;
|
|
88
|
+
if (
|
|
89
|
+
pending === null ||
|
|
90
|
+
pending.process !== owner.process ||
|
|
91
|
+
pending.run !== owner.run ||
|
|
92
|
+
pending.result.cellId !== message.cellId
|
|
93
|
+
)
|
|
94
|
+
return;
|
|
95
|
+
clearTimeout(pending.timer);
|
|
96
|
+
this.#pending = null;
|
|
97
|
+
const report = message.globals.length === 0 ? pending.report : { ...pending.report, globals: message.globals };
|
|
98
|
+
const result = memory.annotateReport(pending.result, report);
|
|
99
|
+
pending.finish(result);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
2
|
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
|
3
|
+
import { killWindowsTree } from "../js/windows-tree-kill-host.ts";
|
|
3
4
|
|
|
4
5
|
export interface SubprocessLike {
|
|
5
6
|
readonly pid?: number;
|
|
6
|
-
readonly stdin: {
|
|
7
|
+
readonly stdin: {
|
|
8
|
+
write(chunk: string): unknown;
|
|
9
|
+
on?(event: "error", listener: (error: Error) => void): unknown;
|
|
10
|
+
};
|
|
7
11
|
readonly stdout: NodeJS.ReadableStream;
|
|
8
12
|
readonly stderr: NodeJS.ReadableStream;
|
|
9
13
|
on(event: "error", listener: (error: Error) => void): this;
|
|
@@ -46,11 +50,13 @@ export class SubprocessProcess {
|
|
|
46
50
|
private readonly stdoutReader: ReadlineInterface;
|
|
47
51
|
private readonly stderrListener: (chunk: string | Buffer) => void;
|
|
48
52
|
private readonly errorListener: (error: Error) => void;
|
|
53
|
+
private readonly stdinErrorListener: (error: Error) => void;
|
|
49
54
|
private exited = false;
|
|
50
55
|
private retiring = false;
|
|
51
56
|
private outputDetached = false;
|
|
52
57
|
private errorReported = false;
|
|
53
58
|
private terminationPromise: Promise<boolean> | null = null;
|
|
59
|
+
private windowsTreeKill: Promise<void> | undefined;
|
|
54
60
|
|
|
55
61
|
constructor(child: SubprocessLike, handlers: SubprocessProcessHandlers) {
|
|
56
62
|
this.child = child;
|
|
@@ -67,8 +73,16 @@ export class SubprocessProcess {
|
|
|
67
73
|
this.stdoutReader.on("line", (line) => {
|
|
68
74
|
if (!this.retiring) this.handlers.onLine(this, `${line}\n`);
|
|
69
75
|
});
|
|
76
|
+
// senpi#3016: a frame written while the child is dying fails on the stdin stream (EPIPE), which emits there
|
|
77
|
+
// and not on the ChildProcess. Unhandled, it is a process-level error that can end the host; once the child is
|
|
78
|
+
// gone or being retired the write failure carries no news, otherwise it is this process's error.
|
|
79
|
+
this.stdinErrorListener = (error) => {
|
|
80
|
+
if (this.exited || this.retiring) return;
|
|
81
|
+
this.errorListener(error);
|
|
82
|
+
};
|
|
70
83
|
child.stderr.on("data", this.stderrListener);
|
|
71
84
|
child.on("error", this.errorListener);
|
|
85
|
+
child.stdin.on?.("error", this.stdinErrorListener);
|
|
72
86
|
child.once("exit", (code, signal) => {
|
|
73
87
|
this.exited = true;
|
|
74
88
|
this.detachOutput();
|
|
@@ -83,7 +97,7 @@ export class SubprocessProcess {
|
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
send(frame: string): boolean {
|
|
86
|
-
if (this.retiring) return false;
|
|
100
|
+
if (this.retiring || this.exited) return false;
|
|
87
101
|
this.child.stdin.write(frame);
|
|
88
102
|
return true;
|
|
89
103
|
}
|
|
@@ -125,6 +139,8 @@ export class SubprocessProcess {
|
|
|
125
139
|
this.retire();
|
|
126
140
|
if (this.exited) return true;
|
|
127
141
|
this.kill(initialSignal);
|
|
142
|
+
// On Windows the kill first lists processes (up to 5 s); the exit wait starts once taskkill has run.
|
|
143
|
+
if (this.windowsTreeKill) await this.windowsTreeKill;
|
|
128
144
|
if (await this.waitForExit(escalationMs)) return true;
|
|
129
145
|
this.kill("SIGKILL");
|
|
130
146
|
return await this.waitForExit(forcedExitWaitMs);
|
|
@@ -151,6 +167,12 @@ export class SubprocessProcess {
|
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
private kill(signal: NodeJS.Signals): void {
|
|
170
|
+
if (globalThis.process.platform === "win32") {
|
|
171
|
+
// taskkill /F is already forced, so escalation reuses the first kill instead of listing again.
|
|
172
|
+
if (this.child.pid !== undefined) this.windowsTreeKill ??= killWindowsTree(this.child.pid);
|
|
173
|
+
else this.child.kill(signal);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
154
176
|
if (this.child.pid !== undefined) {
|
|
155
177
|
try {
|
|
156
178
|
globalThis.process.kill(-this.child.pid, signal);
|
|
@@ -18,6 +18,8 @@ export interface EvalPromptParts {
|
|
|
18
18
|
|
|
19
19
|
export interface EvalPromptOptions {
|
|
20
20
|
readonly spawns: boolean;
|
|
21
|
+
/** `prompt.advertiseHelpers`: true appends one pointer line to the helper documentation entry. */
|
|
22
|
+
readonly advertiseHelpers?: boolean;
|
|
21
23
|
/** Whether the session registry exposes the monitor tool through eval. */
|
|
22
24
|
readonly monitor?: boolean;
|
|
23
25
|
readonly spawnDefaultAgent?: string;
|
|
@@ -74,6 +76,10 @@ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyl
|
|
|
74
76
|
type ContextValue = string | boolean;
|
|
75
77
|
type Context = Readonly<Record<string, ContextValue>>;
|
|
76
78
|
|
|
79
|
+
/** The single line `prompt.advertiseHelpers: true` adds; off by default, so the default description is unchanged. */
|
|
80
|
+
export const ADVERTISED_HELPERS_LINE =
|
|
81
|
+
"Advanced cell helpers (handle controls, wait(), kernel tools in workpools) are documented on demand: tool_schema('eval:helpers').";
|
|
82
|
+
|
|
77
83
|
export function buildEvalPrompt(
|
|
78
84
|
enabled: EnabledLanguages,
|
|
79
85
|
options: EvalPromptOptions = { spawns: false },
|
|
@@ -104,9 +110,10 @@ export function buildEvalPrompt(
|
|
|
104
110
|
maxDetachedCells: String(options.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS),
|
|
105
111
|
kernelPreludeDocs: (options.kernelPreludes ?? []).map((prelude) => prelude.documentation).join("\n"),
|
|
106
112
|
};
|
|
107
|
-
const
|
|
113
|
+
const rendered = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
108
114
|
.replace(/\n{3,}/g, "\n\n")
|
|
109
115
|
.trim();
|
|
116
|
+
const description = options.advertiseHelpers === true ? `${rendered}\n\n${ADVERTISED_HELPERS_LINE}` : rendered;
|
|
110
117
|
return {
|
|
111
118
|
description,
|
|
112
119
|
promptSnippet: "Run one incremental code cell in a persistent language kernel.",
|
package/src/tool/AGENTS.md
CHANGED
|
@@ -17,6 +17,7 @@ anchor most suites).
|
|
|
17
17
|
| Code preview layout | `code-preview.ts` -> `display-code.ts` (JS: `display-js*.ts` Bun-only masked `Bun.Transpiler`; Python: `display-python*.ts` user interpreter, background + repaint) |
|
|
18
18
|
| Status events, execution events | `status-events.ts`, `eval-execution-event.ts` |
|
|
19
19
|
| Interrupt, capture | `interrupt-note.ts`, `call-capture.ts` |
|
|
20
|
+
| Magic cells | `magic-cells.ts` (parsing), `magic-cell-host.ts` (`%pip`, `%bun`/`%npm`, `%environment`), `load-cell.ts` (`%load`); installs live in `../environments/` |
|
|
20
21
|
|
|
21
22
|
## CONVENTIONS
|
|
22
23
|
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
14
14
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
15
15
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
16
16
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
17
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
17
18
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
18
19
|
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
19
20
|
import {
|
|
@@ -54,6 +55,8 @@ export interface CellBridgeRuntime {
|
|
|
54
55
|
readonly handles?: HandleRegistry;
|
|
55
56
|
/** Absolute wall-clock deadline of this cell; a completion handle it creates is bounded by it. */
|
|
56
57
|
readonly hardDeadlineMs?: number;
|
|
58
|
+
/** A JS cell's session environment for `packages.install()`; Python cells reach theirs over the bridge. */
|
|
59
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export class CellHandler {
|
|
@@ -235,22 +238,20 @@ export class CellHandler {
|
|
|
235
238
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
236
239
|
} catch (error) {
|
|
237
240
|
if (!this.#state.active) return;
|
|
238
|
-
const
|
|
239
|
-
error instanceof Error ? error.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
const code =
|
|
242
|
+
error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
+
// A blocked call (a permission denial or another hook's veto) is not an argument problem.
|
|
245
|
+
const text =
|
|
246
|
+
code === "blocked"
|
|
247
|
+
? errorMessage
|
|
248
|
+
: appendSchemaHint(errorMessage, message.toolName, this.#toolParameters(message.toolName));
|
|
243
249
|
recordToolCall(this.#state.toolCalls, false, capture, undefined, text);
|
|
244
250
|
this.#kernel.deliverToolReply({
|
|
245
251
|
type: "tool-reply",
|
|
246
252
|
callId: message.callId,
|
|
247
253
|
ok: false,
|
|
248
|
-
error: {
|
|
249
|
-
message: text,
|
|
250
|
-
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
251
|
-
? { code: error.code }
|
|
252
|
-
: {}),
|
|
253
|
-
},
|
|
254
|
+
error: { message: text, ...(code === undefined ? {} : { code }) },
|
|
254
255
|
});
|
|
255
256
|
}
|
|
256
257
|
this.#resultBuilder.emitUpdate(false);
|
|
@@ -29,6 +29,7 @@ export function reservedDispatchContext(
|
|
|
29
29
|
marshalToolResult,
|
|
30
30
|
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
31
31
|
...(host === undefined ? {} : { evalHandleHost: host }),
|
|
32
|
+
...(runtime.environments === undefined ? {} : { environments: runtime.environments }),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
package/src/tool/cell-runtime.ts
CHANGED
|
@@ -219,8 +219,10 @@ export class CellResultBuilder {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
#liveUpdateText(): string {
|
|
222
|
-
if (this.#state.status === "queued" && this.#state.queuedBehind
|
|
223
|
-
return
|
|
222
|
+
if (this.#state.status === "queued" && this.#state.queuedBehind !== undefined) {
|
|
223
|
+
return this.#state.queuedBehind.length === 0
|
|
224
|
+
? `waiting for the ${this.#state.input.language} kernel to be ready`
|
|
225
|
+
: `queued behind ${this.#state.queuedBehind.join(", ")} in the ${this.#state.input.language} kernel`;
|
|
224
226
|
}
|
|
225
227
|
const summary = this.#state.input.summary === undefined ? "" : ` ${this.#state.input.summary}`;
|
|
226
228
|
const output = this.#liveLines.text();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { WakeSourceState } from "../extension/wake-source-state.ts";
|
|
3
|
+
import type { EvalImageContent } from "./image-resize.ts";
|
|
3
4
|
import type { EvalLanguage, EvalToolDetails } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
export type EvalDetachedCellState = "queued" | "running" | "detached" | "completed" | "failed" | "cancelled";
|
|
@@ -24,6 +25,8 @@ export interface EvalDetachedCellSnapshot {
|
|
|
24
25
|
export interface EvalDetachedCellNotification {
|
|
25
26
|
readonly cellId: string;
|
|
26
27
|
readonly content: string;
|
|
28
|
+
/** Images the cell displayed, delivered with the notification the way a foreground result delivers them. */
|
|
29
|
+
readonly images?: readonly EvalImageContent[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface EvalDetachedCellNotifier {
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
EvalDetachedCellSnapshot,
|
|
14
14
|
EvalDetachedCellStatusEntry,
|
|
15
15
|
} from "./detached-cell-contract.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { cancelledDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
|
|
17
17
|
import {
|
|
18
18
|
activeDetachedCellReuseError,
|
|
19
19
|
allowsDetachedCellTransition,
|
|
@@ -36,7 +36,6 @@ export type {
|
|
|
36
36
|
} from "./detached-cell-contract.ts";
|
|
37
37
|
|
|
38
38
|
export class EvalDetachedCellManager {
|
|
39
|
-
readonly #artifactsDir: string | undefined;
|
|
40
39
|
readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
|
|
41
40
|
readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
|
|
42
41
|
readonly #cells = new Map<string, ManagedCell>();
|
|
@@ -49,7 +48,6 @@ export class EvalDetachedCellManager {
|
|
|
49
48
|
readonly #maxDetachedCells: number;
|
|
50
49
|
|
|
51
50
|
constructor(options: EvalDetachedCellManagerOptions = {}) {
|
|
52
|
-
this.#artifactsDir = options.artifactsDir;
|
|
53
51
|
this.#onStatusChange = options.onStatusChange;
|
|
54
52
|
this.#onWakeSourceState = options.onWakeSourceState;
|
|
55
53
|
this.#notificationQueue = new DetachedNotificationQueue(options.notifier);
|
|
@@ -85,7 +83,6 @@ export class EvalDetachedCellManager {
|
|
|
85
83
|
const cell = createManagedCell({
|
|
86
84
|
cellId,
|
|
87
85
|
input,
|
|
88
|
-
artifactsDir: this.#artifactsDir,
|
|
89
86
|
now: this.#now,
|
|
90
87
|
defaultHardLimitSeconds: this.#hardLimitSeconds,
|
|
91
88
|
defaultRunBudgetSeconds: this.#runBudgetSeconds,
|
|
@@ -239,7 +236,6 @@ export class EvalDetachedCellManager {
|
|
|
239
236
|
await cell.interruptOutcome?.promise;
|
|
240
237
|
return this.#snapshot(cell);
|
|
241
238
|
},
|
|
242
|
-
spillPath: cell.spillPath,
|
|
243
239
|
});
|
|
244
240
|
}
|
|
245
241
|
}
|
|
@@ -260,7 +256,7 @@ export class EvalDetachedCellManager {
|
|
|
260
256
|
return;
|
|
261
257
|
}
|
|
262
258
|
if (foreground) {
|
|
263
|
-
if (this.#settle(cell, "cancelled",
|
|
259
|
+
if (this.#settle(cell, "cancelled", cancelledDetachedResult(cell))) cell.onKill?.(expiry.error);
|
|
264
260
|
return;
|
|
265
261
|
}
|
|
266
262
|
await this.#cancel(cell, expiry.error.message);
|
|
@@ -269,7 +265,7 @@ export class EvalDetachedCellManager {
|
|
|
269
265
|
#cancelQueued(cell: ManagedCell, reason: string, error = new Error(reason)): void {
|
|
270
266
|
const dequeued = cell.kernel?.cancelQueued(cell.cellId, reason) ?? false;
|
|
271
267
|
cell.stateRetained = true;
|
|
272
|
-
this.#settle(cell, "cancelled",
|
|
268
|
+
this.#settle(cell, "cancelled", cancelledDetachedResult(cell));
|
|
273
269
|
// Acquisition/reset has no queue promise yet; its execution has no interrupt target either.
|
|
274
270
|
if (!dequeued) cell.onKill?.(error);
|
|
275
271
|
}
|
|
@@ -278,7 +274,7 @@ export class EvalDetachedCellManager {
|
|
|
278
274
|
const outcome = Promise.withResolvers<void>();
|
|
279
275
|
cell.interruptOutcome = outcome;
|
|
280
276
|
try {
|
|
281
|
-
if (!this.#settle(cell, "cancelled",
|
|
277
|
+
if (!this.#settle(cell, "cancelled", cancelledDetachedResult(cell)) || cell.kernel === undefined) return;
|
|
282
278
|
const handle = await cell.kernel.interrupt(reason, cell.cellId);
|
|
283
279
|
cell.interruptNote = handle.note;
|
|
284
280
|
cell.stateRetained = await handle.stateRetained;
|
|
@@ -1,61 +1,21 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
1
|
import type { EvalDetachedCellNotification, EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
|
|
4
2
|
import { interruptionStateNote, unknownInterruptionStateNote } from "./interrupt-note.ts";
|
|
5
3
|
import type { EvalKernelState, EvalMemoryDetails } from "./types.ts";
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export async function buildDetachedCellNotification(
|
|
15
|
-
snapshot: EvalDetachedCellSnapshot,
|
|
16
|
-
spillPath: string | undefined,
|
|
17
|
-
): Promise<EvalDetachedCellNotification> {
|
|
18
|
-
const body = notificationBody(snapshot);
|
|
19
|
-
const overflow = Buffer.byteLength(body, "utf8") > NOTIFICATION_TAIL_BYTES;
|
|
20
|
-
let spillNotice = "";
|
|
21
|
-
if (overflow && spillPath !== undefined) {
|
|
22
|
-
try {
|
|
23
|
-
await mkdir(dirname(spillPath), { recursive: true });
|
|
24
|
-
await writeFile(spillPath, body, "utf8");
|
|
25
|
-
// The agent read tool resolves plain paths only, so the notice must carry
|
|
26
|
-
// the absolute spill path, never the kernel-helper local:// scheme.
|
|
27
|
-
spillNotice = `\nBuffered output overflowed; full output: ${spillPath}`;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
-
spillNotice = `\nBuffered output overflow could not be spilled: ${message}`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
cellId: snapshot.cellId,
|
|
35
|
-
content: `${overflow ? notificationPreview(snapshot) : body}${overflow ? "\n[…notification tail capped…]" : ""}${spillNotice}`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function notificationBody(cell: EvalDetachedCellSnapshot): string {
|
|
40
|
-
const outcome = outcomeOf(cell);
|
|
41
|
-
const resultText = textContent(cell);
|
|
42
|
-
const stateNote = stateNoteOf(cell);
|
|
43
|
-
return [
|
|
44
|
-
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
|
|
45
|
-
resultText.length === 0 ? "(no output)" : resultText,
|
|
46
|
-
`${stateNote}</system-reminder>`,
|
|
47
|
-
].join("\n");
|
|
5
|
+
/**
|
|
6
|
+
* A detached cell's completion carries the same text its result would have shown in the foreground: the output sink
|
|
7
|
+
* already bounded it (the configured head, the tail, the middle marker and the artifact notice), plus its images.
|
|
8
|
+
*/
|
|
9
|
+
export function buildDetachedCellNotification(snapshot: EvalDetachedCellSnapshot): EvalDetachedCellNotification {
|
|
10
|
+
const images = snapshot.result.content.filter((part) => part.type === "image");
|
|
11
|
+
return { cellId: snapshot.cellId, content: notificationText(snapshot, textContent(snapshot)), images };
|
|
48
12
|
}
|
|
49
13
|
|
|
50
|
-
function
|
|
51
|
-
const outcome = outcomeOf(cell);
|
|
52
|
-
const resultText = textContent(cell);
|
|
53
|
-
const stateNote = stateNoteOf(cell);
|
|
14
|
+
function notificationText(cell: EvalDetachedCellSnapshot, output: string): string {
|
|
54
15
|
return [
|
|
55
|
-
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
`${stateNote}</system-reminder>`,
|
|
16
|
+
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcomeOf(cell)}.`,
|
|
17
|
+
output.length === 0 ? "(no output)" : output,
|
|
18
|
+
`${stateNoteOf(cell)}</system-reminder>`,
|
|
59
19
|
].join("\n");
|
|
60
20
|
}
|
|
61
21
|
|
|
@@ -101,15 +61,3 @@ export function memoryStateNote(memory: EvalMemoryDetails | undefined): string {
|
|
|
101
61
|
return "The kernel was restarted before this cell ran - globals from earlier cells are gone; this cell's variables are available to the next eval cell.";
|
|
102
62
|
return "Kernel state updated - variables are available to the next eval cell.";
|
|
103
63
|
}
|
|
104
|
-
|
|
105
|
-
function safeCellId(cellId: string): string {
|
|
106
|
-
return cellId.replace(/[^a-zA-Z0-9_-]/gu, "_");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function truncateTailUtf8(text: string, maxBytes: number): string {
|
|
110
|
-
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
111
|
-
const bytes = Buffer.from(text, "utf8");
|
|
112
|
-
let start = bytes.length - maxBytes;
|
|
113
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
|
|
114
|
-
return bytes.subarray(start).toString("utf8");
|
|
115
|
-
}
|