@code-yeongyu/senpi-codemode 2026.10.10 → 2026.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/README.md +113 -11
  3. package/package.json +6 -5
  4. package/src/bridge/memory-protocol.ts +8 -1
  5. package/src/bridge/protocol.ts +9 -4
  6. package/src/bridge/reserved.ts +2 -0
  7. package/src/bridges/eval-environment-schemas.ts +52 -0
  8. package/src/bridges/eval-virtual-schemas.ts +11 -4
  9. package/src/bridges/reserved-dispatch.ts +13 -1
  10. package/src/config/executable-settings.json +5 -0
  11. package/src/config/project-trust.ts +51 -0
  12. package/src/config/settings-overrides.ts +2 -0
  13. package/src/environments/editable-check.ts +1 -1
  14. package/src/environments/packages-install.ts +98 -0
  15. package/src/environments/py-environment.ts +7 -3
  16. package/src/environments/py-installer.ts +5 -1
  17. package/src/environments/python-environments.ts +1 -1
  18. package/src/extension/bridge-tool-call.ts +5 -1
  19. package/src/extension/eval-notifier.ts +13 -2
  20. package/src/extension/kernel-registration.ts +7 -2
  21. package/src/extension/runtime-factory.ts +42 -13
  22. package/src/extension/runtime-info.ts +6 -2
  23. package/src/extension/session-cwd.ts +25 -9
  24. package/src/extension/session-manager-contract.ts +3 -0
  25. package/src/extension/session-manager.ts +20 -6
  26. package/src/index.ts +2 -2
  27. package/src/interpreters/detect.ts +34 -1
  28. package/src/kernels/AGENTS.md +2 -0
  29. package/src/kernels/jl/globals.jl +128 -0
  30. package/src/kernels/jl/prelude.jl +15 -2
  31. package/src/kernels/jl/runner.jl +35 -143
  32. package/src/kernels/js/cell-ownership.js +268 -0
  33. package/src/kernels/js/cell-run-context.js +89 -0
  34. package/src/kernels/js/context-manager.ts +23 -24
  35. package/src/kernels/js/control-frames.ts +24 -0
  36. package/src/kernels/js/group-signal-notice.js +117 -0
  37. package/src/kernels/js/inline-worker.ts +2 -0
  38. package/src/kernels/js/kernel-contract.ts +16 -1
  39. package/src/kernels/js/kernel-memory-bridge.ts +1 -1
  40. package/src/kernels/js/kernel-memory.ts +79 -0
  41. package/src/kernels/js/kernel-tools-registry.js +1 -0
  42. package/src/kernels/js/local-module-loader.ts +8 -0
  43. package/src/kernels/js/process-drain-scan.d.ts +8 -0
  44. package/src/kernels/js/process-drain-scan.js +28 -0
  45. package/src/kernels/js/process-entry.js +466 -0
  46. package/src/kernels/js/process-memory-host.ts +83 -0
  47. package/src/kernels/js/process-tree-host.ts +4 -8
  48. package/src/kernels/js/process-tree.d.ts +11 -0
  49. package/src/kernels/js/process-tree.js +55 -10
  50. package/src/kernels/js/process-worker.ts +298 -0
  51. package/src/kernels/js/rejection-reports.js +61 -0
  52. package/src/kernels/js/run-queue.ts +7 -0
  53. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  54. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  55. package/src/kernels/js/windows-tree-kill.js +100 -0
  56. package/src/kernels/js/worker-core.js +51 -8
  57. package/src/kernels/js/worker-cwd.js +46 -12
  58. package/src/kernels/js/worker-host.ts +9 -2
  59. package/src/kernels/js/worker-indirect-eval.js +37 -13
  60. package/src/kernels/js/worker-require.d.ts +19 -0
  61. package/src/kernels/js/worker-require.js +52 -0
  62. package/src/kernels/js/worker-runtime.js +66 -13
  63. package/src/kernels/js/worker-shadow-guard.js +2 -6
  64. package/src/kernels/js/worker-shell-capture.js +9 -0
  65. package/src/kernels/js/worker-slot.ts +8 -0
  66. package/src/kernels/js/worker-startup.ts +49 -1
  67. package/src/kernels/js/worker-webview.js +15 -1
  68. package/src/kernels/py/prelude.py +69 -27
  69. package/src/kernels/py/process.ts +4 -0
  70. package/src/kernels/py/transport.ts +4 -0
  71. package/src/kernels/rb/runner.rb +9 -19
  72. package/src/kernels/sandbox/sandbox-cell.ts +26 -1
  73. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  74. package/src/kernels/shared/bun-ffi.ts +24 -0
  75. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  76. package/src/kernels/shared/process-cpu.ts +7 -3
  77. package/src/kernels/shared/process-group-cpu.ts +6 -3
  78. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  79. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  80. package/src/kernels/shared/subprocess-process.ts +24 -2
  81. package/src/kernels/shared/subprocess-queue.ts +1 -0
  82. package/src/prompt/eval-prompt.ts +8 -1
  83. package/src/tool/AGENTS.md +1 -0
  84. package/src/tool/cell-handler.ts +3 -0
  85. package/src/tool/cell-reserved-dispatch.ts +1 -0
  86. package/src/tool/cell-runtime.ts +4 -2
  87. package/src/tool/detached-cell-contract.ts +3 -0
  88. package/src/tool/detached-cell-manager.ts +4 -8
  89. package/src/tool/detached-cell-notification.ts +11 -63
  90. package/src/tool/detached-cell-snapshot.ts +17 -0
  91. package/src/tool/detached-eval-result.ts +13 -7
  92. package/src/tool/detached-notification-queue.ts +1 -2
  93. package/src/tool/eval-tool.ts +4 -2
  94. package/src/tool/interrupt-note.ts +4 -5
  95. package/src/tool/live-headline.ts +60 -0
  96. package/src/tool/managed-cell.ts +0 -4
  97. package/src/tool/render-agent.ts +110 -0
  98. package/src/tool/render-blocks.ts +252 -0
  99. package/src/tool/render-call.ts +130 -0
  100. package/src/tool/render-cell.ts +165 -0
  101. package/src/tool/render-detail.ts +152 -0
  102. package/src/tool/render-live.ts +288 -0
  103. package/src/tool/render-result.ts +202 -0
  104. package/src/tool/render-status.ts +166 -0
  105. package/src/tool/render-tail.ts +139 -0
  106. package/src/tool/render.ts +3 -1069
  107. package/src/tool/run-eval-cell.ts +24 -14
  108. package/src/tool/runtime-label.ts +2 -1
  109. package/src/tool/types.ts +13 -2
@@ -1,6 +1,6 @@
1
1
  import type { ResolvedSandbox } from "../../config/feature-settings.ts";
2
2
  import { marshalToolResult } from "../../tool/image.ts";
3
- import type { ExecuteTool, HostCellExecutor } from "../../tool/types.ts";
3
+ import type { EvalRuntimeInfo, ExecuteTool, HostCellExecutor } from "../../tool/types.ts";
4
4
  import type { CodemodeSandbox } from "./vendor/pi-codemode/runtime/host.ts";
5
5
  import type { CodemodeError, CodemodeOutputFrame, CodemodeTool } from "./vendor/pi-codemode/types.ts";
6
6
 
@@ -13,6 +13,31 @@ export interface SandboxCellOptions {
13
13
  readonly wasmPath?: string;
14
14
  }
15
15
 
16
+ /**
17
+ * The runtime an isolated cell's result reports: the QuickJS build it runs on, never the persistent kernel's (senpi#2811).
18
+ * Read from the installed quickjs-wasi package the wasm loads from, only when an isolated cell runs; when that package
19
+ * cannot be resolved the cell itself fails with eval_isolate_unavailable, and the version says so.
20
+ */
21
+ export function sandboxRuntimeInfo(): EvalRuntimeInfo {
22
+ let version = "unavailable";
23
+ try {
24
+ // getBuiltinModule, not a static import: node:module would otherwise load with the extension.
25
+ const manifest: unknown = process.getBuiltinModule("node:module").createRequire(import.meta.url)(
26
+ "quickjs-wasi/package.json",
27
+ );
28
+ if (
29
+ typeof manifest === "object" &&
30
+ manifest !== null &&
31
+ "version" in manifest &&
32
+ typeof manifest.version === "string"
33
+ )
34
+ version = manifest.version;
35
+ } catch {
36
+ // Reported as "unavailable": the same missing package makes the cell fail with eval_isolate_unavailable.
37
+ }
38
+ return { name: "quickjs", version, isolation: "sandbox" };
39
+ }
40
+
16
41
  // On the script's first line so reported line numbers still match the cell; globals, so a cell's own
17
42
  // `const print` shadows them instead of failing as a redeclaration.
18
43
  const OUTPUT_ALIASES =
@@ -115,7 +115,7 @@ class Execution {
115
115
  private finished = false;
116
116
  // senpi-change begin: output streaming
117
117
  private readonly stream: StreamOptions | undefined;
118
- private readonly credits: Int32Array | undefined;
118
+ private readonly credits: Int32Array<SharedArrayBuffer> | undefined;
119
119
  private frameChain: Promise<void> = Promise.resolve();
120
120
  private frames = 0;
121
121
  private inFlightBytes = 0;
@@ -188,7 +188,7 @@ class Execution {
188
188
  // senpi-change begin: output streaming and builtin policy
189
189
  ...(this.credits === undefined || this.stream === undefined
190
190
  ? {}
191
- : { stream: { credits: this.credits.buffer as SharedArrayBuffer, frameBytes: this.stream.frameBytes } }),
191
+ : { stream: { credits: this.credits.buffer, frameBytes: this.stream.frameBytes } }),
192
192
  storePolicy: options.storePolicy,
193
193
  // senpi-change end
194
194
  };
@@ -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 still names its largest globals, so the
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
- const report =
41
- this.#readFootprint === undefined
42
- ? runnerReport
43
- : this.#footprintReport(this.#readFootprint, pid, runnerReport);
44
- return report === undefined ? result : { ...result, memory: this.#policy.annotate(report) };
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 = process.getBuiltinModule("bun:ffi") as BunFfi | undefined;
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("kernel32.dll", {
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 = process.getBuiltinModule("bun:ffi") as BunFfi | undefined;
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("libSystem.B.dylib", {
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 ownResult = message.type === "result" && this.runs.active?.input.cellId === message.cellId;
252
- const settled = ownResult && this.memory ? this.memory.annotate(message, process.child.pid) : message;
253
- if (!this.runs.handleMessage(settled, this.onMessage)) return;
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: { write(chunk: string): unknown };
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);
@@ -120,6 +120,7 @@ export class SubprocessRunQueue {
120
120
  return false;
121
121
  case "webview-connect":
122
122
  case "memory-query-result":
123
+ case "memory-globals-result":
123
124
  case "kernel-tools-defined":
124
125
  return false;
125
126
  default: {
@@ -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 description = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
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.",
@@ -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
 
@@ -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 {
@@ -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
 
@@ -219,8 +219,10 @@ export class CellResultBuilder {
219
219
  }
220
220
 
221
221
  #liveUpdateText(): string {
222
- if (this.#state.status === "queued" && this.#state.queuedBehind?.length) {
223
- return `queued behind ${this.#state.queuedBehind.join(", ")} in the ${this.#state.input.language} kernel`;
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 { currentDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
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", currentDetachedResult(cell))) cell.onKill?.(expiry.error);
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", currentDetachedResult(cell));
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", currentDetachedResult(cell)) || cell.kernel === undefined) return;
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;