@code-yeongyu/senpi-codemode 2026.10.10-3 → 2026.10.10-4

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 CHANGED
@@ -12,6 +12,24 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.10.10-4] - 2026-10-06
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - A detached eval cell that waits for its kernel to start now says so (`waiting for the js kernel to be ready`) instead of `queued behind in the js kernel` with an empty predecessor ([#2790](https://github.com/code-yeongyu/senpi/issues/2790)).
26
+
27
+ - A stopped (or failed) detached cell's result and notification show its buffered output instead of the live `1/1 cells running` frame, so a cancelled cell no longer reads as still running. The kernel-state note now says plainly whether the kernel was restarted: `The JavaScript worker was not restarted; variables from earlier cells are kept.` instead of `... remains running; its existing variables are preserved.`, or `The JavaScript worker was restarted; variables from earlier cells are lost.` instead of `... was unresponsive to interrupt and was restarted ...` (the worker may have answered the interrupt and still needed a restart) ([#2791](https://github.com/code-yeongyu/senpi/issues/2791)).
28
+
29
+ - An isolated (`isolate: true`) eval cell's result names QuickJS as its runtime (`quickjs <version>, sandbox`) instead of the persistent kernel's Bun or Node runtime ([#2811](https://github.com/code-yeongyu/senpi/issues/2811)).
30
+
31
+ ### Removed
32
+
15
33
  ## [2026.10.10-3] - 2026-10-06
16
34
 
17
35
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.10.10-3",
3
+ "version": "2026.10.10-4",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -33,15 +33,15 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@babel/parser": "8.0.4",
36
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-3",
36
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-4",
37
37
  "quickjs-wasi": "3.6.2",
38
38
  "typebox": "1.3.34"
39
39
  },
40
40
  "peerDependencies": {
41
- "@code-yeongyu/senpi": "2026.10.10-3"
41
+ "@code-yeongyu/senpi": "2026.10.10-4"
42
42
  },
43
43
  "devDependencies": {
44
- "@code-yeongyu/senpi": "2026.10.10-3"
44
+ "@code-yeongyu/senpi": "2026.10.10-4"
45
45
  },
46
46
  "keywords": [
47
47
  "senpi",
@@ -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 =
@@ -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();
@@ -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,
@@ -260,7 +260,7 @@ export class EvalDetachedCellManager {
260
260
  return;
261
261
  }
262
262
  if (foreground) {
263
- if (this.#settle(cell, "cancelled", currentDetachedResult(cell))) cell.onKill?.(expiry.error);
263
+ if (this.#settle(cell, "cancelled", cancelledDetachedResult(cell))) cell.onKill?.(expiry.error);
264
264
  return;
265
265
  }
266
266
  await this.#cancel(cell, expiry.error.message);
@@ -269,7 +269,7 @@ export class EvalDetachedCellManager {
269
269
  #cancelQueued(cell: ManagedCell, reason: string, error = new Error(reason)): void {
270
270
  const dequeued = cell.kernel?.cancelQueued(cell.cellId, reason) ?? false;
271
271
  cell.stateRetained = true;
272
- this.#settle(cell, "cancelled", currentDetachedResult(cell));
272
+ this.#settle(cell, "cancelled", cancelledDetachedResult(cell));
273
273
  // Acquisition/reset has no queue promise yet; its execution has no interrupt target either.
274
274
  if (!dequeued) cell.onKill?.(error);
275
275
  }
@@ -278,7 +278,7 @@ export class EvalDetachedCellManager {
278
278
  const outcome = Promise.withResolvers<void>();
279
279
  cell.interruptOutcome = outcome;
280
280
  try {
281
- if (!this.#settle(cell, "cancelled", currentDetachedResult(cell)) || cell.kernel === undefined) return;
281
+ if (!this.#settle(cell, "cancelled", cancelledDetachedResult(cell)) || cell.kernel === undefined) return;
282
282
  const handle = await cell.kernel.interrupt(reason, cell.cellId);
283
283
  cell.interruptNote = handle.note;
284
284
  cell.stateRetained = await handle.stateRetained;
@@ -63,6 +63,23 @@ export function currentDetachedResult(cell: DetachedCellResultSource): AgentTool
63
63
  return cell.terminalResult ?? cell.liveResult?.() ?? fallbackResult(cell.input);
64
64
  }
65
65
 
66
+ /**
67
+ * The result a cell cancelled before its handler finished settles with: its buffered output, not the live progress
68
+ * frame ("1/1 cells running"), so a stopped cell never reads as still running.
69
+ */
70
+ export function cancelledDetachedResult(cell: DetachedCellResultSource): AgentToolResult<EvalToolDetails> {
71
+ if (cell.terminalResult !== undefined) return cell.terminalResult;
72
+ const current = currentDetachedResult(cell);
73
+ const output = detachedOutputTail(current);
74
+ return {
75
+ content: [
76
+ { type: "text", text: output.length > 0 ? output : "(no output)" },
77
+ ...current.content.filter((part) => part.type === "image"),
78
+ ],
79
+ details: current.details,
80
+ };
81
+ }
82
+
66
83
  export function detachedErrorResult(cell: DetachedCellResultSource, error: Error): AgentToolResult<EvalToolDetails> {
67
84
  const current = currentDetachedResult(cell);
68
85
  const output = detachedOutputTail(current);
@@ -46,9 +46,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
46
46
  language: snapshot.language,
47
47
  state: snapshot.state,
48
48
  startedAtMs: snapshot.startedAtMs,
49
- ...(snapshot.queuedBehind === undefined || snapshot.queuedBehind.length === 0
50
- ? {}
51
- : { queuedBehind: [...snapshot.queuedBehind] }),
49
+ ...(snapshot.queuedBehind === undefined ? {} : { queuedBehind: [...snapshot.queuedBehind] }),
52
50
  ...(summary ? { summary } : {}),
53
51
  };
54
52
  });
@@ -60,7 +58,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
60
58
  " ",
61
59
  );
62
60
  const elapsed = Math.floor(snapshot.result.details.durationMs / 1000);
63
- const queued = cell.queuedBehind === undefined ? "" : ` queued behind ${cell.queuedBehind.join(", ")}`;
61
+ const queued = cell.queuedBehind === undefined ? "" : ` ${queuedPhrase(cell.queuedBehind, cell.language)}`;
64
62
  return `${cell.cellId} ${cell.language} ${cell.state} ${elapsed}s${queued} - ${preview}`;
65
63
  })
66
64
  .join("\n");
@@ -76,11 +74,13 @@ export function resultAfterDetach(
76
74
  otherLiveCells: number,
77
75
  ): AgentToolResult<EvalToolDetails> {
78
76
  if (snapshot.state !== "detached" && snapshot.state !== "running") return createDetachedControlResult(snapshot);
79
- const predecessors = snapshot.queuedBehind?.join(", ");
77
+ const queuedBehind = snapshot.queuedBehind;
80
78
  const text =
81
- predecessors === undefined
79
+ queuedBehind === undefined
82
80
  ? `Eval cell ${snapshot.cellId} detached and is running in the ${input.language} kernel (${otherLiveCells} other live cells). Completion arrives as a notification; do not re-run it. eval({ action: "peek" | "stop", cell_id }) or eval({ action: "list" }).`
83
- : `Eval cell ${snapshot.cellId} is queued behind ${predecessors} in the ${input.language} kernel and detached; it runs after ${predecessors} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
81
+ : queuedBehind.length === 0
82
+ ? `Eval cell ${snapshot.cellId} is detached and waiting for the ${input.language} kernel to be ready; it runs first once the kernel is ready and completes as one notification. peek/stop/list with eval({ action, cell_id })`
83
+ : `Eval cell ${snapshot.cellId} is queued behind ${queuedBehind.join(", ")} in the ${input.language} kernel and detached; it runs after ${queuedBehind.join(", ")} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
84
84
  return {
85
85
  content: [
86
86
  {
@@ -164,6 +164,12 @@ export function resultForDetachedState(
164
164
  };
165
165
  }
166
166
 
167
+ function queuedPhrase(queuedBehind: readonly string[], language: string): string {
168
+ return queuedBehind.length === 0
169
+ ? `waiting for the ${language} kernel to be ready`
170
+ : `queued behind ${queuedBehind.join(", ")}`;
171
+ }
172
+
167
173
  function textContent(result: AgentToolResult<EvalToolDetails>): string {
168
174
  return result.content
169
175
  .filter((part) => part.type === "text")
@@ -26,9 +26,8 @@ export async function describeTimeoutState(
26
26
  );
27
27
  if (outcome === undefined) error.message = fallbackTimeoutMessage(error.message);
28
28
  else if (outcome.retained)
29
- error.message = `${error.message} The kernel remains running; its existing variables are preserved.`;
30
- else
31
- error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
29
+ error.message = `${error.message} The kernel was not restarted; variables from earlier cells are kept.`;
30
+ else error.message = `${error.message} The kernel was restarted; variables from earlier cells are lost.`;
32
31
  if (outcome?.note !== undefined) error.message = `${error.message} ${outcome.note.trim()}`;
33
32
  return error;
34
33
  }
@@ -63,8 +62,8 @@ const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
63
62
  export function interruptionStateNote(language: EvalLanguage, stateRetained: boolean | undefined): string | undefined {
64
63
  if (stateRetained === undefined) return undefined;
65
64
  const label = LANGUAGE_LABEL[language];
66
- if (stateRetained) return `${label} was interrupted and remains running; its existing variables are preserved.`;
67
- return `${label} was unresponsive to interrupt and was restarted; variables from earlier cells are lost.`;
65
+ if (stateRetained) return `The ${label} was not restarted; variables from earlier cells are kept.`;
66
+ return `The ${label} was restarted; variables from earlier cells are lost.`;
68
67
  }
69
68
 
70
69
  export function unknownInterruptionStateNote(language: EvalLanguage): string {
@@ -344,6 +344,8 @@ function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges
344
344
  let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
345
345
  if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
346
346
  header += ` · queued behind ${cell.queuedBehind.map(sanitizeTerminalLabel).join(", ")}`;
347
+ else if (cell.queuedBehind !== undefined && cell.status === "queued")
348
+ header += ` · waiting for the ${cell.language} kernel to be ready`;
347
349
  const throughputBadge = badges.throughput === undefined ? undefined : formatThroughputBadge(badges.throughput);
348
350
  if (throughputBadge !== undefined) header += ` · ${throughputBadge}`;
349
351
  const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
@@ -14,7 +14,7 @@ import {
14
14
  type KernelToolsCapability,
15
15
  type KernelToolsDescribeResult,
16
16
  } from "../kernels/js/kernel-tools-types.ts";
17
- import { sandboxCellExecutor } from "../kernels/sandbox/sandbox-cell.ts";
17
+ import { sandboxCellExecutor, sandboxRuntimeInfo } from "../kernels/sandbox/sandbox-cell.ts";
18
18
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
19
19
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
20
20
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -44,7 +44,8 @@ export async function runEvalCell(
44
44
  const bridgeAbortController = new AbortController();
45
45
  const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
46
46
  const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
47
- const runtime = options.runtimes?.[invocation.input.language];
47
+ const runtime =
48
+ invocation.input.isolate === true ? sandboxRuntimeInfo() : options.runtimes?.[invocation.input.language];
48
49
  const state: CellState = {
49
50
  input: invocation.input,
50
51
  ...(runtime === undefined ? {} : { runtime }),
@@ -11,7 +11,7 @@ const ELLIPSIS = "\u2026";
11
11
  */
12
12
  export function formatRuntimeBadge(language: EvalLanguage, runtime: EvalRuntimeInfo, home: string = homedir()): string {
13
13
  const base = language === "js" ? `${runtime.name} ${runtime.version}` : runtime.version;
14
- const label = runtime.isolation === "process" ? `${base}, process` : base;
14
+ const label = runtime.isolation === undefined ? base : `${base}, ${runtime.isolation}`;
15
15
  if (runtime.path === undefined || runtime.path.length === 0) return label;
16
16
  return `${label}, ${minifyPath(runtime.path, home)}`;
17
17
  }
package/src/tool/types.ts CHANGED
@@ -279,8 +279,8 @@ export interface EvalRuntimeInfo {
279
279
  readonly name: string;
280
280
  readonly version: string;
281
281
  readonly path?: string;
282
- /** JS only: the kernel runs in a child process instead of a worker thread. */
283
- readonly isolation?: "process";
282
+ /** JS only: the kernel runs in a child process (`process`), or the cell ran in an isolated QuickJS VM (`sandbox`). */
283
+ readonly isolation?: "process" | "sandbox";
284
284
  }
285
285
 
286
286
  export type EvalRuntimes = Readonly<Partial<Record<EvalLanguage, EvalRuntimeInfo>>>;