@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10-2

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 (60) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +103 -7
  3. package/package.json +6 -5
  4. package/src/bridge/http-server.ts +9 -1
  5. package/src/bridge/protocol.ts +4 -0
  6. package/src/bridges/eval-virtual-schemas.ts +1 -0
  7. package/src/config/executable-settings.json +5 -0
  8. package/src/config/project-trust.ts +51 -0
  9. package/src/config/settings-overrides.ts +2 -0
  10. package/src/environments/js-environments.ts +208 -0
  11. package/src/environments/js-installer.ts +268 -0
  12. package/src/environments/js-revision-files.ts +91 -0
  13. package/src/environments/no-links.ts +132 -0
  14. package/src/environments/project-resolves.ts +54 -0
  15. package/src/environments/py-installer.ts +64 -4
  16. package/src/environments/python-environments.ts +85 -0
  17. package/src/environments/revision-store.ts +38 -7
  18. package/src/extension/bridge-tool-call.ts +2 -0
  19. package/src/extension/cell-kernel-tools.ts +28 -0
  20. package/src/extension/kernel-replacement.ts +20 -0
  21. package/src/extension/runtime-factory.ts +44 -2
  22. package/src/extension/session-manager-proxy.ts +6 -0
  23. package/src/extension/session-manager.ts +18 -3
  24. package/src/index.ts +2 -0
  25. package/src/interpreters/detect.ts +34 -1
  26. package/src/kernels/jl/prelude.jl +6 -1
  27. package/src/kernels/js/cell-dispatch.ts +44 -0
  28. package/src/kernels/js/context-manager.ts +25 -15
  29. package/src/kernels/js/host-entries.ts +62 -0
  30. package/src/kernels/js/kernel-contract.ts +2 -0
  31. package/src/kernels/js/kernel-tools-registry.js +1 -0
  32. package/src/kernels/js/local-module-loader.ts +21 -5
  33. package/src/kernels/js/worker-core.js +3 -0
  34. package/src/kernels/js/worker-package-resolve.js +153 -0
  35. package/src/kernels/js/worker-runtime.js +4 -1
  36. package/src/kernels/js/worker-startup.ts +1 -0
  37. package/src/kernels/js/workpool.js +5 -2
  38. package/src/kernels/py/host-cell.ts +8 -31
  39. package/src/kernels/py/kernel.ts +8 -2
  40. package/src/kernels/py/prelude.py +122 -11
  41. package/src/kernels/py/process.ts +4 -0
  42. package/src/kernels/py/transport.ts +13 -1
  43. package/src/kernels/rb/workpool.rb +7 -1
  44. package/src/kernels/sandbox/sandbox-cell.ts +116 -0
  45. package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
  46. package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
  47. package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
  48. package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
  49. package/src/kernels/shared/cell-source-at-start.ts +18 -0
  50. package/src/kernels/shared/host-cell.ts +57 -0
  51. package/src/prompt/eval-prompt.ts +8 -1
  52. package/src/tool/cell-handler.ts +10 -12
  53. package/src/tool/eval-request.ts +23 -1
  54. package/src/tool/eval-tool-options.ts +6 -0
  55. package/src/tool/eval-tool.ts +17 -4
  56. package/src/tool/load-cell.ts +88 -0
  57. package/src/tool/magic-cell-host.ts +123 -0
  58. package/src/tool/magic-cells.ts +75 -0
  59. package/src/tool/run-eval-cell.ts +57 -0
  60. package/src/tool/types.ts +48 -1
@@ -0,0 +1,62 @@
1
+ import type { KernelToHostMessage } from "../../bridge/protocol.ts";
2
+ import type { HostCellExecutor } from "../../tool/types.ts";
3
+ import { runHostCell } from "../shared/host-cell.ts";
4
+ import type { ResultMessage } from "./kernel-contract.ts";
5
+
6
+ export interface HostEntryKernel<Run> {
7
+ emit(run: Run, message: KernelToHostMessage): void;
8
+ settle(run: Run, result: ResultMessage): void;
9
+ durationMs(run: Run): number;
10
+ }
11
+
12
+ /** The host entries (installs) a JavaScript kernel is running: started per queue slot, stopped by interrupt and close. */
13
+ export class HostEntries<Run extends { readonly input: { readonly cellId: string } }> {
14
+ readonly #aborts = new Map<Run, AbortController>();
15
+ readonly #done = new Map<Run, Promise<unknown>>();
16
+ readonly #kernel: HostEntryKernel<Run>;
17
+
18
+ constructor(kernel: HostEntryKernel<Run>) {
19
+ this.#kernel = kernel;
20
+ }
21
+
22
+ /** Runs `host` for `run`'s queue slot; it holds the slot like a cell but never reaches the worker. */
23
+ start(run: Run, host: HostCellExecutor): void {
24
+ const entry = runHostCell(run.input.cellId, host, {
25
+ emit: (message) => this.#kernel.emit(run, message),
26
+ settle: (result) => {
27
+ this.#aborts.delete(run);
28
+ this.#kernel.settle(run, result);
29
+ },
30
+ durationMs: () => this.#kernel.durationMs(run),
31
+ settleAfterAbort: true,
32
+ });
33
+ this.#aborts.set(run, entry.abort);
34
+ this.#done.set(run, entry.done);
35
+ void entry.done.finally(() => this.#done.delete(run));
36
+ }
37
+
38
+ /** Aborts `run`'s host entry; false when `run` is not one. */
39
+ abort(run: Run, reason: string): boolean {
40
+ const abort = this.#aborts.get(run);
41
+ if (abort === undefined) return false;
42
+ abort.abort(new Error(reason));
43
+ return true;
44
+ }
45
+
46
+ /** Aborts every running entry and waits for them to stop, at most `ms`: a closed kernel leaves no install running. */
47
+ async stopAll(reason: string, ms: number): Promise<void> {
48
+ const running = [...this.#done.values()];
49
+ for (const abort of this.#aborts.values()) abort.abort(new Error(reason));
50
+ let timer: ReturnType<typeof setTimeout> | undefined;
51
+ await Promise.race([
52
+ Promise.allSettled(running),
53
+ new Promise<void>((resolve) => (timer = setTimeout(resolve, ms))),
54
+ ]);
55
+ if (timer !== undefined) clearTimeout(timer);
56
+ }
57
+ }
58
+
59
+ /** A turn-time refusal (an unreadable `%load` file, a refused magic) settles in queue order as a failed host entry. */
60
+ export function refusedEntry(message: string): HostCellExecutor {
61
+ return async () => ({ ok: false, error: { message } });
62
+ }
@@ -32,6 +32,8 @@ export interface JavaScriptKernelOptions {
32
32
  readonly sessionEnv?: SessionEnvironment;
33
33
  /** Host tool names denied as JS kernel-tool identifiers (init protocol). */
34
34
  readonly hostToolNames?: KernelToolNameSource;
35
+ /** `kernelTools.enabled`; false makes `tool(fn)` refuse with `tools_unavailable`. Unset keeps them on. */
36
+ readonly kernelToolsEnabled?: boolean;
35
37
  /** Tool names registered in another kernel language, denied as JS kernel-tool identifiers. */
36
38
  readonly foreignLanguageNames?: KernelToolNameSource;
37
39
  /** Post-cell collection, notice, and ceiling thresholds; absent leaves the worker's memory unmanaged. */
@@ -45,6 +45,7 @@ export function createKernelToolRegistry(options = {}) {
45
45
  return generation;
46
46
  },
47
47
  define(fn, metadata) {
48
+ if (options.disabled === true) throw kernelToolError("tools_unavailable", "kernel tools are turned off for this project (kernelTools.enabled is false)");
48
49
  assertJs();
49
50
  const parsed = parseToolFunction(fn);
50
51
  const resolved = resolveToolMetadata(metadata, parsed.params);
@@ -1,4 +1,4 @@
1
- import { join, resolve, sep } from "node:path";
1
+ import { dirname, join, resolve, sep } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
4
4
  import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
@@ -98,7 +98,13 @@ function loaderPrelude(context: RuntimeModuleContext): string {
98
98
  " const urlModule = await import('node:url');",
99
99
  " target = urlModule.pathToFileURL(specifier).href;",
100
100
  " }",
101
- " return options === undefined ? import(target) : import(target, options);",
101
+ " const load = (resolved) => (options === undefined ? import(resolved) : import(resolved, options));",
102
+ " const resolvePackage = globalThis[Symbol.for('senpi.kernel.resolvePackage')];",
103
+ " if (target === specifier && typeof resolvePackage === 'function') {",
104
+ " const resolved = resolvePackage(specifier, [context.cwdUrl, context.packageRootUrl]);",
105
+ " if (resolved !== undefined) return await load(resolved);",
106
+ " }",
107
+ " return await load(target);",
102
108
  "};",
103
109
  ].join("\n");
104
110
  }
@@ -115,14 +121,24 @@ function contributionPrelude(plan: KernelPreludePlan): string {
115
121
 
116
122
  export class LocalModuleLoader {
117
123
  readonly #prelude: string;
124
+ readonly #cwdUrl: string;
118
125
  readonly #contributions = new KernelPreludeTracker();
119
126
 
120
127
  constructor(options: LocalModuleLoaderOptions) {
121
128
  this.#prelude = loaderPrelude(runtimeContext(options));
129
+ this.#cwdUrl = directoryUrl(options.cwd);
122
130
  }
123
131
 
124
- prepareCell(code: string, contributions: readonly KernelPreludeContribution[] = []): string {
125
- const prelude = `${this.#prelude}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
126
- return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code) })}`;
132
+ prepareCell(
133
+ code: string,
134
+ contributions: readonly KernelPreludeContribution[] = [],
135
+ sourceFile?: string,
136
+ packageRoot?: string,
137
+ ): string {
138
+ // A %load cell resolves its relative imports from its own file's directory for that cell only.
139
+ const cwdUrl = sourceFile === undefined ? this.#cwdUrl : directoryUrl(dirname(sourceFile));
140
+ const base = `\nglobalThis.__senpi_module_context__ = { ...globalThis.__senpi_module_context__, cwdUrl: ${JSON.stringify(cwdUrl)}, packageRootUrl: ${JSON.stringify(packageRoot === undefined ? null : directoryUrl(packageRoot))} };`;
141
+ const prelude = `${this.#prelude}${base}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
142
+ return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code), ...(sourceFile === undefined ? {} : { sourceFile }) })}`;
127
143
  }
128
144
  }
@@ -3,6 +3,7 @@ import { kernelToolError } from "./kernel-tools-errors.js";
3
3
  import { createKernelToolPump } from "./kernel-tools-pump.js";
4
4
  import { hostDeniedError, hostToolRefusal } from "./kernel-tools-scope.js";
5
5
  import { installSessionCwd } from "./worker-cwd.js";
6
+ import { installPackageResolver } from "./worker-package-resolve.js";
6
7
  import { createHeapProbe } from "./worker-heap.js";
7
8
  import { createWorkerMemory } from "./worker-memory.js";
8
9
  import { JsWorkerRuntime } from "./worker-runtime.js";
@@ -129,6 +130,7 @@ export function createWorkerCore(transport, options) {
129
130
  applySessionEnvironment(message.sessionEnv);
130
131
  installSessionCwd(options.cwd);
131
132
  installKernelWebView(requestWebViewPort);
133
+ installPackageResolver();
132
134
  runtime = new JsWorkerRuntime({
133
135
  cwd: options.cwd,
134
136
  parallelPoolWidth: options.parallelPoolWidth,
@@ -137,6 +139,7 @@ export function createWorkerCore(transport, options) {
137
139
  kernelGeneration: message.kernelGeneration ?? 1,
138
140
  hostToolNames: message.hostToolNames ?? [],
139
141
  foreignLanguageNames: message.foreignLanguageNames ?? [],
142
+ kernelToolsDisabled: message.kernelToolsDisabled === true,
140
143
  onChildEvent: (event) => emit({ type: "status", event: { op: CHILD_LIFECYCLE_OP, ...event } }),
141
144
  onShellWaitChange: () => {
142
145
  if (activeCell?.interruption) acknowledgeInterrupt();
@@ -0,0 +1,153 @@
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { isBuiltin } from "node:module";
3
+ import { dirname, join, sep } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ // A cell's bare import runs `import()` from this worker's own module, so the runtime resolves it from Senpi's install
7
+ // tree, not the session's project or its managed package revision. This resolver walks `node_modules` from each root
8
+ // the cell names, in order, and resolves the package entry the way ESM does (the `exports` field with import
9
+ // conditions), so ESM-only packages load and the first root that has the package wins.
10
+ const RESOLVE_PACKAGE = Symbol.for("senpi.kernel.resolvePackage");
11
+ const CONDITIONS = typeof globalThis.Bun === "object" ? ["bun", "node", "import", "default"] : ["node", "import", "default"];
12
+
13
+ function isFile(path) {
14
+ try {
15
+ return statSync(path).isFile();
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function isDirectory(path) {
22
+ try {
23
+ return statSync(path).isDirectory();
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function splitSpecifier(specifier) {
30
+ const parts = specifier.split("/");
31
+ const nameParts = specifier.startsWith("@") ? 2 : 1;
32
+ if (parts.length < nameParts || parts.slice(0, nameParts).some((part) => part === "" || part === "." || part === ".."))
33
+ return undefined;
34
+ const rest = parts.slice(nameParts).join("/");
35
+ return { name: parts.slice(0, nameParts).join("/"), subpath: rest === "" ? "." : `./${rest}` };
36
+ }
37
+
38
+ function conditionalTarget(target) {
39
+ if (typeof target === "string") return target;
40
+ if (Array.isArray(target)) {
41
+ for (const candidate of target) {
42
+ const resolved = conditionalTarget(candidate);
43
+ if (resolved !== undefined) return resolved;
44
+ }
45
+ return undefined;
46
+ }
47
+ if (target === null || typeof target !== "object") return undefined;
48
+ for (const [condition, value] of Object.entries(target)) {
49
+ if (condition !== "default" && !CONDITIONS.includes(condition)) continue;
50
+ const resolved = conditionalTarget(value);
51
+ if (resolved !== undefined) return resolved;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function exportsTarget(exportsField, subpath) {
57
+ const subpathKeyed =
58
+ exportsField !== null &&
59
+ typeof exportsField === "object" &&
60
+ !Array.isArray(exportsField) &&
61
+ Object.keys(exportsField).some((key) => key.startsWith("."));
62
+ if (!subpathKeyed) return subpath === "." ? conditionalTarget(exportsField) : undefined;
63
+ if (Object.hasOwn(exportsField, subpath)) return conditionalTarget(exportsField[subpath]);
64
+ let best;
65
+ for (const key of Object.keys(exportsField)) {
66
+ const star = key.indexOf("*");
67
+ if (star < 0 || key.includes("*", star + 1)) continue;
68
+ const prefix = key.slice(0, star);
69
+ const suffix = key.slice(star + 1);
70
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length)
71
+ continue;
72
+ if (best === undefined || prefix.length > best.prefix.length) {
73
+ best = { key, prefix, match: subpath.slice(prefix.length, subpath.length - suffix.length) };
74
+ }
75
+ }
76
+ if (best === undefined || hasInvalidSegment(best.match)) return undefined;
77
+ const target = conditionalTarget(exportsField[best.key]);
78
+ return target === undefined ? undefined : target.replaceAll("*", best.match);
79
+ }
80
+
81
+ // Node refuses a pattern match or target with "." / ".." / "node_modules" segments (ERR_INVALID_MODULE_SPECIFIER).
82
+ function hasInvalidSegment(path) {
83
+ return path.split(/[\\/]/).some((segment) => segment === "." || segment === ".." || segment === "node_modules");
84
+ }
85
+
86
+ function legacyFile(path) {
87
+ for (const candidate of [path, `${path}.js`, `${path}.mjs`, `${path}.cjs`, join(path, "index.js")]) {
88
+ if (isFile(candidate)) return candidate;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ function packageEntry(packageDir, subpath, specifier) {
94
+ let manifest;
95
+ try {
96
+ manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
97
+ } catch {
98
+ return legacyFile(join(packageDir, subpath === "." ? "index.js" : subpath));
99
+ }
100
+ if (manifest.exports !== undefined && manifest.exports !== null) {
101
+ const target = exportsTarget(manifest.exports, subpath);
102
+ if (typeof target !== "string" || !target.startsWith("./") || hasInvalidSegment(target.slice(2))) {
103
+ const error = new Error(`Package subpath '${subpath}' is not exported for import by '${specifier}'`);
104
+ error.code = "ERR_PACKAGE_PATH_NOT_EXPORTED";
105
+ throw error;
106
+ }
107
+ return join(packageDir, target);
108
+ }
109
+ if (subpath !== ".") return legacyFile(join(packageDir, subpath));
110
+ const moduleEntry = typeof globalThis.Bun === "object" && typeof manifest.module === "string" ? manifest.module : undefined;
111
+ const main = moduleEntry ?? (typeof manifest.main === "string" ? manifest.main : "index.js");
112
+ return legacyFile(join(packageDir, main));
113
+ }
114
+
115
+ function findPackageDir(name, rootUrl) {
116
+ let directory = fileURLToPath(rootUrl);
117
+ for (;;) {
118
+ const candidate = join(directory, "node_modules", name);
119
+ if (isDirectory(candidate)) return candidate;
120
+ const parent = dirname(directory);
121
+ if (parent === directory) return undefined;
122
+ directory = parent;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * The file URL the bare `specifier` resolves to from the first of `rootUrls` whose `node_modules` chain has its
128
+ * package, or undefined when it names a builtin or no root has the package (the caller then imports it natively).
129
+ */
130
+ export function resolvePackage(specifier, rootUrls) {
131
+ if (specifier.includes(":") || isBuiltin(specifier)) return undefined;
132
+ const parts = splitSpecifier(specifier);
133
+ if (parts === undefined) return undefined;
134
+ for (const rootUrl of rootUrls) {
135
+ if (typeof rootUrl !== "string") continue;
136
+ const packageDir = findPackageDir(parts.name, rootUrl);
137
+ if (packageDir === undefined) continue;
138
+ const entry = packageEntry(packageDir, parts.subpath, specifier);
139
+ if (entry === undefined) continue;
140
+ let real = entry;
141
+ try {
142
+ real = realpathSync(entry);
143
+ } catch {
144
+ // A missing target fails in the import itself, with the runtime's own error.
145
+ }
146
+ return pathToFileURL(real.endsWith(sep) ? real.slice(0, -1) : real).href;
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ export function installPackageResolver() {
152
+ globalThis[RESOLVE_PACKAGE] = resolvePackage;
153
+ }
@@ -47,6 +47,7 @@ export class JsWorkerRuntime {
47
47
  generation: options.kernelGeneration ?? 1,
48
48
  hostToolNames: options.hostToolNames ?? [],
49
49
  foreignLanguageNames: options.foreignLanguageNames ?? [],
50
+ disabled: options.kernelToolsDisabled === true,
50
51
  });
51
52
  this.#installGlobals();
52
53
  }
@@ -65,13 +66,15 @@ export class JsWorkerRuntime {
65
66
  try {
66
67
  let prelude = "";
67
68
  let cellCode = code;
69
+ let sourceName = cellId;
68
70
  if (code.startsWith(PREPARED_CELL_PREFIX)) {
69
71
  const prepared = JSON.parse(code.slice(PREPARED_CELL_PREFIX.length));
70
72
  if (!isPlainObject(prepared) || typeof prepared.prelude !== "string" || typeof prepared.code !== "string") throw new Error("Invalid prepared JavaScript cell payload");
71
73
  ({ prelude, code: cellCode } = prepared);
74
+ if (typeof prepared.sourceFile === "string") sourceName = prepared.sourceFile;
72
75
  }
73
76
  if (prelude) indirectEval(prelude, PRELUDE_SOURCE_URL);
74
- const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)), cellId));
77
+ const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)), sourceName));
75
78
  await this.#drainPendingDisplays();
76
79
  return value;
77
80
  } finally {
@@ -68,6 +68,7 @@ async function initializeWorker(worker: WorkerLike, hooks: WorkerStartupHooks, s
68
68
  foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
69
69
  ...(options.sessionEnv === undefined ? {} : { sessionEnv: options.sessionEnv }),
70
70
  ...(options.memory === undefined ? {} : { memory: options.memory }),
71
+ ...(options.kernelToolsEnabled === false ? { kernelToolsDisabled: true } : {}),
71
72
  });
72
73
  await ready;
73
74
  }
@@ -4,8 +4,11 @@ import { kernelToolError } from "./kernel-tools-errors.js";
4
4
  // Only host-tool sugar: no admission, worker, or queue state belongs to the kernel.
5
5
  export async function createWorkpool(callTool, agent, name, options = {}) {
6
6
  if (inKernelToolInvoke()) throw kernelToolError("kernel_tool_recursion", "kernel tools may not invoke workpool()");
7
- if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "mode")) {
8
- throw new TypeError("workpool() options only accept mode");
7
+ if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "mode" && key !== "tools")) {
8
+ throw new TypeError("workpool() options only accept mode and tools");
9
+ }
10
+ if (options.tools !== undefined && (!Array.isArray(options.tools) || !options.tools.every(name => typeof name === "string"))) {
11
+ throw workpoolError("invalid_tools", `workpool() tools takes an array of tool names; got ${Array.isArray(options.tools) ? "an array with a non-string" : typeof options.tools}`);
9
12
  }
10
13
  const call = async args => {
11
14
  try {
@@ -1,42 +1,19 @@
1
1
  import type { KernelToHostMessage } from "../../bridge/protocol.ts";
2
2
  import type { HostCellExecutor } from "../../tool/types.ts";
3
-
4
- type HostCellOutcome = Awaited<ReturnType<HostCellExecutor>>;
5
-
3
+ import { runHostCell as runSharedHostCell } from "../shared/host-cell.ts";
6
4
  import type { PendingRun, ResultMessage } from "./kernel-contract.ts";
7
- import { failedPythonResult } from "./transport.ts";
8
5
 
9
6
  export function runHostCell(
10
7
  pending: PendingRun,
11
8
  host: HostCellExecutor,
12
9
  io: { readonly emit: (message: KernelToHostMessage) => void; readonly settle: (result: ResultMessage) => void },
13
10
  ): void {
14
- const abort = new AbortController();
15
- pending.hostAbort = abort;
16
- const cellId = pending.input.cellId;
17
- let finished = false;
18
- // Output from an executor that was aborted, or after its outcome, belongs to no cell.
19
- const emit = (message: KernelToHostMessage): void => {
20
- if (!finished && !abort.signal.aborted) io.emit(message);
21
- };
22
- // Calling the executor inside a promise keeps a synchronous throw in this cell instead of failing the queue.
23
- const done = new Promise<HostCellOutcome>((resolve) => resolve(host({ signal: abort.signal, emit }))).then(
24
- (outcome): ResultMessage =>
25
- outcome.ok
26
- ? {
27
- type: "result",
28
- cellId,
29
- ok: true,
30
- durationMs: 0,
31
- ...(outcome.valueRepr === undefined ? {} : { valueRepr: outcome.valueRepr }),
32
- }
33
- : { type: "result", cellId, ok: false, error: outcome.error, durationMs: 0 },
34
- (error: unknown): ResultMessage =>
35
- failedPythonResult(cellId, error instanceof Error ? error.message : String(error)),
36
- );
37
- pending.hostDone = done;
38
- void done.then((result) => {
39
- finished = true;
40
- if (!abort.signal.aborted) io.settle(result);
11
+ const run = runSharedHostCell(pending.input.cellId, host, {
12
+ emit: io.emit,
13
+ settle: io.settle,
14
+ durationMs: () => 0,
15
+ settleAfterAbort: false,
41
16
  });
17
+ pending.hostAbort = run.abort;
18
+ pending.hostDone = run.done;
42
19
  }
@@ -5,6 +5,7 @@ import type {
5
5
  KernelToolsInvokeOptions,
6
6
  KernelToolsInvokeRequest,
7
7
  } from "../js/kernel-tools-types.ts";
8
+ import { inputAtStart } from "../shared/cell-source-at-start.ts";
8
9
  import { describeExit } from "../shared/kernel-death.ts";
9
10
  import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
10
11
  import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
@@ -243,10 +244,15 @@ export class PythonKernel {
243
244
  });
244
245
  return;
245
246
  }
247
+ const input = inputAtStart(pending.input);
248
+ if ("refused" in input) {
249
+ this.#settleRun(pending, failedPythonResult(pending.input.cellId, input.refused));
250
+ return;
251
+ }
246
252
  try {
247
253
  this.#transport?.run({
248
- ...pending.input,
249
- preludePlan: this.#preludes.plan(pending.input.kernelPreludes ?? []),
254
+ ...input,
255
+ preludePlan: this.#preludes.plan(input.kernelPreludes ?? []),
250
256
  });
251
257
  } catch (error) {
252
258
  const failure = error instanceof Error ? error : new Error(String(error));