@code-yeongyu/senpi-codemode 2026.10.10-10 → 2026.10.10-12
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 +46 -0
- package/package.json +4 -4
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +1 -1
- package/src/bridges/eval-environment-schemas.ts +5 -1
- package/src/environments/editable-check.ts +1 -1
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager.ts +4 -4
- package/src/kernels/jl/runner.jl +11 -22
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/inline-worker.ts +2 -0
- package/src/kernels/js/local-module-loader.ts +8 -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/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-cwd.js +41 -10
- package/src/kernels/js/worker-runtime.js +23 -4
- package/src/kernels/js/worker-shell-capture.js +4 -0
- package/src/kernels/js/worker-slot.ts +2 -3
- package/src/kernels/py/prelude.py +36 -25
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/shared/bun-ffi.ts +24 -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 +20 -12
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/tool/eval-tool.ts +2 -1
- 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 -1128
- package/src/tool/run-eval-cell.ts +7 -12
- package/src/tool/types.ts +11 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read of `bun:ffi` behind a guard: `process.getBuiltinModule` returns the module on Bun and
|
|
3
|
+
* `undefined` on Node, so this module loads on either runtime, and only a result carrying the callable
|
|
4
|
+
* surface the CPU readers bind is treated as FFI - the same boundary `display-js.ts` uses for `Bun`.
|
|
5
|
+
*/
|
|
6
|
+
export type BunFfi = {
|
|
7
|
+
readonly dlopen: <S>(path: string, definitions: Record<string, unknown>) => { readonly symbols: S };
|
|
8
|
+
readonly FFIType: Record<string, number>;
|
|
9
|
+
readonly ptr: (buffer: ArrayBufferView) => number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** True when `value` carries the `bun:ffi` surface the kernel CPU readers use (`dlopen`, `FFIType`, `ptr`). */
|
|
13
|
+
export function isBunFfi(value: unknown): value is BunFfi {
|
|
14
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15
|
+
if (!("dlopen" in value) || typeof value.dlopen !== "function") return false;
|
|
16
|
+
if (!("ptr" in value) || typeof value.ptr !== "function") return false;
|
|
17
|
+
return "FFIType" in value && typeof value.FFIType === "object" && value.FFIType !== null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The runtime's `bun:ffi` module, or `undefined` on Node and on any runtime whose builtin is not one. */
|
|
21
|
+
export function loadBunFfi(): BunFfi | undefined {
|
|
22
|
+
const ffi: unknown = process.getBuiltinModule("bun:ffi");
|
|
23
|
+
return isBunFfi(ffi) ? ffi : undefined;
|
|
24
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
|
|
1
|
+
import type { KernelMemoryReport, KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
|
|
2
2
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
3
|
import type { EvalLanguage } from "../../tool/types.ts";
|
|
4
4
|
import { KernelMemoryPolicy } from "./kernel-memory.ts";
|
|
@@ -11,7 +11,7 @@ export type FootprintReader = (pid: number) => { readonly bytes: number } | unde
|
|
|
11
11
|
export interface KernelMemoryHostOptions {
|
|
12
12
|
/**
|
|
13
13
|
* Host-measured kernel (rb, jl): the host reads the interpreter's footprint after each result and
|
|
14
|
-
* replaces the runner's live reading with it; the runner
|
|
14
|
+
* replaces the runner's live reading with it; the runner names its largest globals on demand, so the
|
|
15
15
|
* notice and the ceiling both apply, with every reading counting (no collection precedes it).
|
|
16
16
|
*/
|
|
17
17
|
readonly readFootprint?: FootprintReader;
|
|
@@ -36,12 +36,35 @@ export class KernelMemoryHost {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
annotate(result: ResultMessage, pid?: number): ResultMessage {
|
|
39
|
+
return this.annotateReport(result, this.readReport(result, pid));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readReport(result: ResultMessage, pid?: number): KernelMemoryReport | undefined {
|
|
39
43
|
const runnerReport = result.memory;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
return this.#readFootprint === undefined
|
|
45
|
+
? runnerReport
|
|
46
|
+
: this.#footprintReport(this.#readFootprint, pid, runnerReport);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
needsGlobals(report: KernelMemoryReport): boolean {
|
|
50
|
+
const { noticeBytes, ceilingBytes } = this.#policy.thresholds;
|
|
51
|
+
// Collect at or above the host-read notice threshold or ceiling, regardless of notice hysteresis;
|
|
52
|
+
// never collect below both thresholds. Zero disables a threshold, not the lazy diagnostic gate.
|
|
53
|
+
return (
|
|
54
|
+
(noticeBytes > 0 && report.liveBytes >= noticeBytes) || (ceilingBytes > 0 && report.liveBytes >= ceilingBytes)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
annotateReport(result: ResultMessage, report: KernelMemoryReport | undefined): ResultMessage {
|
|
59
|
+
if (report === undefined) return result;
|
|
60
|
+
const memory = this.#policy.annotate(report);
|
|
61
|
+
// Safety accounting still observes every cell, but ordinary host-measured results carry no payload.
|
|
62
|
+
// A recycle announcement remains visible even when the replacement process is below threshold.
|
|
63
|
+
if (this.#readFootprint !== undefined && !this.needsGlobals(report) && memory.notice === undefined) {
|
|
64
|
+
const { memory: _memory, ...ordinary } = result;
|
|
65
|
+
return ordinary;
|
|
66
|
+
}
|
|
67
|
+
return { ...result, memory };
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
/**
|
|
@@ -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);
|
|
@@ -152,7 +168,8 @@ export class SubprocessProcess {
|
|
|
152
168
|
|
|
153
169
|
private kill(signal: NodeJS.Signals): void {
|
|
154
170
|
if (globalThis.process.platform === "win32") {
|
|
155
|
-
|
|
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);
|
|
156
173
|
else this.child.kill(signal);
|
|
157
174
|
return;
|
|
158
175
|
}
|
|
@@ -168,15 +185,6 @@ export class SubprocessProcess {
|
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
// Windows has no process-group signal: taskkill /T /F walks the tree rooted at the child.
|
|
172
|
-
function killProcessTree(pid: number): void {
|
|
173
|
-
try {
|
|
174
|
-
nodeSpawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
175
|
-
} catch (error) {
|
|
176
|
-
if (!(error instanceof Error)) throw error;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
188
|
export function spawnSubprocess(spawn: SubprocessSpawn | undefined, request: SubprocessSpawnRequest): SubprocessLike {
|
|
181
189
|
if (spawn) return spawn(request.command, request.args, { cwd: request.cwd, env: request.env });
|
|
182
190
|
const child = nodeSpawn(request.command, [...request.args], {
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type EvalListInput,
|
|
26
26
|
type EvalResultDetails,
|
|
27
27
|
type EvalToolDetails,
|
|
28
|
+
type EvalToolInput,
|
|
28
29
|
type EvalToolRequest,
|
|
29
30
|
enabledLanguageList,
|
|
30
31
|
} from "./types.ts";
|
|
@@ -34,7 +35,7 @@ export type { CreateEvalToolOptions } from "./eval-tool-options.ts";
|
|
|
34
35
|
export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./types.ts";
|
|
35
36
|
|
|
36
37
|
type EvalExecuteArgs<Request extends EvalToolRequest> = Parameters<
|
|
37
|
-
ToolDefinition<TUnsafe<Request
|
|
38
|
+
ToolDefinition<TUnsafe<Request | Partial<EvalToolInput>>, EvalToolDetails>["execute"]
|
|
38
39
|
>;
|
|
39
40
|
|
|
40
41
|
export function createEvalTool(options: CreateEvalToolOptions) {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendLines,
|
|
3
|
+
assertNever,
|
|
4
|
+
type RenderEnvironment,
|
|
5
|
+
renderPrefixed,
|
|
6
|
+
type StatusPresentation,
|
|
7
|
+
spinner,
|
|
8
|
+
style,
|
|
9
|
+
} from "./render-blocks.ts";
|
|
10
|
+
import { eventString } from "./render-status.ts";
|
|
11
|
+
import { formatDuration } from "./tool-widgets.ts";
|
|
12
|
+
import type { EvalStatusEvent } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
type AgentStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
|
15
|
+
|
|
16
|
+
function eventNumber(value: unknown): number {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function agentStatus(value: unknown): AgentStatus {
|
|
21
|
+
switch (value) {
|
|
22
|
+
case "pending":
|
|
23
|
+
case "running":
|
|
24
|
+
case "completed":
|
|
25
|
+
case "failed":
|
|
26
|
+
case "aborted":
|
|
27
|
+
return value;
|
|
28
|
+
default:
|
|
29
|
+
return "running";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function coalesceAgentEvents(events: readonly EvalStatusEvent[]): EvalStatusEvent[] {
|
|
34
|
+
const rows: EvalStatusEvent[] = [];
|
|
35
|
+
const indexes = new Map<string, number>();
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
const id = eventString(event.id);
|
|
38
|
+
if (id === undefined) {
|
|
39
|
+
rows.push(event);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const index = indexes.get(id);
|
|
43
|
+
if (index === undefined) {
|
|
44
|
+
indexes.set(id, rows.length);
|
|
45
|
+
rows.push(event);
|
|
46
|
+
} else rows[index] = event;
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function agentPresentation(status: AgentStatus, spinnerFrame: number | undefined): StatusPresentation {
|
|
52
|
+
switch (status) {
|
|
53
|
+
case "pending":
|
|
54
|
+
return { label: "pending", icon: "○", color: "muted" };
|
|
55
|
+
case "running":
|
|
56
|
+
return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
|
|
57
|
+
case "completed":
|
|
58
|
+
return { label: "done", icon: "✓", color: "success" };
|
|
59
|
+
case "failed":
|
|
60
|
+
return { label: "failed", icon: "✗", color: "error" };
|
|
61
|
+
case "aborted":
|
|
62
|
+
return { label: "aborted", icon: "×", color: "error" };
|
|
63
|
+
default:
|
|
64
|
+
return assertNever(status);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderAgentProgressEvents(
|
|
69
|
+
events: readonly EvalStatusEvent[],
|
|
70
|
+
environment: RenderEnvironment,
|
|
71
|
+
): string[] {
|
|
72
|
+
const rows = coalesceAgentEvents(events);
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
// Senpi Theme has no tree-token API; fixed ├/└/│ glyphs intentionally mirror omp.
|
|
75
|
+
for (const [index, event] of rows.entries()) {
|
|
76
|
+
const isLast = index === rows.length - 1;
|
|
77
|
+
const status = agentStatus(event.status);
|
|
78
|
+
const presentation = agentPresentation(status, environment.spinnerFrame);
|
|
79
|
+
const id = eventString(event.id) ?? "agent";
|
|
80
|
+
const styledId = environment.theme === undefined ? id : environment.theme.bold(id);
|
|
81
|
+
let body = `${style(environment.theme, presentation.color, presentation.icon)} ${styledId} ${presentation.label}`;
|
|
82
|
+
if (status === "completed" || status === "failed" || status === "aborted") {
|
|
83
|
+
const duration = eventNumber(event.durationMs);
|
|
84
|
+
if (duration > 0) body += ` · ${style(environment.theme, "dim", formatDuration(duration))}`;
|
|
85
|
+
}
|
|
86
|
+
const branch = isLast ? "└ " : "├ ";
|
|
87
|
+
const continuation = isLast ? " " : "│ ";
|
|
88
|
+
appendLines(
|
|
89
|
+
lines,
|
|
90
|
+
renderPrefixed(body, environment, { prefix: branch, continuation: continuation, color: "dim" }),
|
|
91
|
+
);
|
|
92
|
+
if (status !== "running") continue;
|
|
93
|
+
const currentTool = eventString(event.currentTool);
|
|
94
|
+
const lastIntent = eventString(event.lastIntent);
|
|
95
|
+
if (currentTool === undefined && lastIntent === undefined) continue;
|
|
96
|
+
const detail =
|
|
97
|
+
currentTool === undefined
|
|
98
|
+
? (lastIntent ?? "")
|
|
99
|
+
: `${currentTool}${lastIntent === undefined ? "" : `: ${lastIntent}`}`;
|
|
100
|
+
appendLines(
|
|
101
|
+
lines,
|
|
102
|
+
renderPrefixed(detail, environment, {
|
|
103
|
+
prefix: `${continuation}└ `,
|
|
104
|
+
continuation: `${continuation} `,
|
|
105
|
+
color: "dim",
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
}
|