@code-yeongyu/senpi-codemode 2026.10.10 → 2026.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +113 -11
  3. package/package.json +7 -6
  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 +80 -44
  17. package/src/environments/python-environments.ts +1 -1
  18. package/src/extension/bridge-tool-call.ts +5 -1
  19. package/src/extension/eval-cells-rpc.ts +62 -0
  20. package/src/extension/eval-notifier.ts +13 -2
  21. package/src/extension/kernel-registration.ts +7 -2
  22. package/src/extension/runtime-factory.ts +42 -13
  23. package/src/extension/runtime-info.ts +6 -2
  24. package/src/extension/session-cwd.ts +25 -9
  25. package/src/extension/session-manager-contract.ts +3 -0
  26. package/src/extension/session-manager.ts +20 -6
  27. package/src/index.ts +8 -3
  28. package/src/interpreters/detect.ts +34 -1
  29. package/src/kernels/AGENTS.md +2 -0
  30. package/src/kernels/jl/globals.jl +128 -0
  31. package/src/kernels/jl/prelude.jl +15 -2
  32. package/src/kernels/jl/runner.jl +35 -143
  33. package/src/kernels/js/cell-ownership.js +268 -0
  34. package/src/kernels/js/cell-run-context.js +89 -0
  35. package/src/kernels/js/context-manager.ts +23 -24
  36. package/src/kernels/js/control-frames.ts +24 -0
  37. package/src/kernels/js/group-signal-notice.js +117 -0
  38. package/src/kernels/js/inline-worker.ts +2 -0
  39. package/src/kernels/js/kernel-contract.ts +16 -1
  40. package/src/kernels/js/kernel-memory-bridge.ts +1 -1
  41. package/src/kernels/js/kernel-memory.ts +79 -0
  42. package/src/kernels/js/kernel-tools-registry.js +1 -0
  43. package/src/kernels/js/local-module-loader.ts +8 -0
  44. package/src/kernels/js/process-drain-scan.d.ts +8 -0
  45. package/src/kernels/js/process-drain-scan.js +28 -0
  46. package/src/kernels/js/process-entry.js +466 -0
  47. package/src/kernels/js/process-memory-host.ts +83 -0
  48. package/src/kernels/js/process-tree-host.ts +4 -8
  49. package/src/kernels/js/process-tree.d.ts +11 -0
  50. package/src/kernels/js/process-tree.js +55 -10
  51. package/src/kernels/js/process-worker.ts +298 -0
  52. package/src/kernels/js/rejection-reports.js +61 -0
  53. package/src/kernels/js/run-queue.ts +7 -0
  54. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  55. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  56. package/src/kernels/js/windows-tree-kill.js +100 -0
  57. package/src/kernels/js/worker-core.js +51 -8
  58. package/src/kernels/js/worker-cwd.js +46 -12
  59. package/src/kernels/js/worker-host.ts +9 -2
  60. package/src/kernels/js/worker-indirect-eval.js +37 -13
  61. package/src/kernels/js/worker-require.d.ts +19 -0
  62. package/src/kernels/js/worker-require.js +52 -0
  63. package/src/kernels/js/worker-runtime.js +66 -13
  64. package/src/kernels/js/worker-shadow-guard.js +2 -6
  65. package/src/kernels/js/worker-shell-capture.js +9 -0
  66. package/src/kernels/js/worker-slot.ts +8 -0
  67. package/src/kernels/js/worker-startup.ts +49 -1
  68. package/src/kernels/js/worker-webview.js +15 -1
  69. package/src/kernels/py/prelude.py +69 -27
  70. package/src/kernels/py/process.ts +4 -0
  71. package/src/kernels/py/transport.ts +4 -0
  72. package/src/kernels/rb/runner.rb +9 -19
  73. package/src/kernels/sandbox/sandbox-cell.ts +26 -1
  74. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  75. package/src/kernels/shared/bun-ffi.ts +24 -0
  76. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  77. package/src/kernels/shared/process-cpu.ts +7 -3
  78. package/src/kernels/shared/process-group-cpu.ts +6 -3
  79. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  80. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  81. package/src/kernels/shared/subprocess-process.ts +24 -2
  82. package/src/kernels/shared/subprocess-queue.ts +1 -0
  83. package/src/prompt/eval-prompt.ts +8 -1
  84. package/src/tool/AGENTS.md +1 -0
  85. package/src/tool/cell-handler.ts +3 -0
  86. package/src/tool/cell-reserved-dispatch.ts +1 -0
  87. package/src/tool/cell-runtime.ts +4 -2
  88. package/src/tool/detached-cell-contract.ts +3 -0
  89. package/src/tool/detached-cell-manager.ts +20 -11
  90. package/src/tool/detached-cell-notification.ts +11 -63
  91. package/src/tool/detached-cell-snapshot.ts +17 -0
  92. package/src/tool/detached-eval-result.ts +21 -10
  93. package/src/tool/detached-notification-queue.ts +1 -2
  94. package/src/tool/eval-tool.ts +4 -2
  95. package/src/tool/interrupt-note.ts +4 -5
  96. package/src/tool/live-headline.ts +60 -0
  97. package/src/tool/managed-cell.ts +0 -4
  98. package/src/tool/render-agent.ts +110 -0
  99. package/src/tool/render-blocks.ts +252 -0
  100. package/src/tool/render-call.ts +130 -0
  101. package/src/tool/render-cell.ts +165 -0
  102. package/src/tool/render-detail.ts +152 -0
  103. package/src/tool/render-live.ts +288 -0
  104. package/src/tool/render-result.ts +202 -0
  105. package/src/tool/render-status.ts +166 -0
  106. package/src/tool/render-tail.ts +139 -0
  107. package/src/tool/render.ts +3 -1069
  108. package/src/tool/run-eval-cell.ts +24 -14
  109. package/src/tool/runtime-label.ts +2 -1
  110. package/src/tool/types.ts +13 -2
@@ -2,6 +2,8 @@ import { type ExtensionContext, withBundledBunCommands } from "@code-yeongyu/sen
2
2
  import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
3
3
  import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
4
4
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
5
+ import { resolveJsIsolation } from "../config/feature-settings.ts";
6
+ import { withoutUntrustedInterpreter } from "../config/project-trust.ts";
5
7
  import {
6
8
  type CodemodeSettings,
7
9
  loadCodemodeSettings,
@@ -16,6 +18,7 @@ import {
16
18
  getInterpreterAvailability,
17
19
  type InterpreterAvailability,
18
20
  } from "../interpreters/detect.ts";
21
+ import { processRuntimeInfo } from "../kernels/js/process-worker.ts";
19
22
  import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
20
23
  import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
21
24
  import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
@@ -62,12 +65,25 @@ export async function createRuntime(
62
65
  handles?: HandleRegistry,
63
66
  ): Promise<SessionRuntime> {
64
67
  const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
68
+ const trusted = withoutUntrustedInterpreter(
69
+ loaded.settings,
70
+ loaded.source,
71
+ ctx.cwd,
72
+ typeof ctx.isProjectTrusted === "function" ? () => ctx.isProjectTrusted() : undefined,
73
+ );
65
74
  const settings: ResolvedCodemodeSettings = {
66
- ...loaded.settings,
67
- languages: resolveEnabledLanguages(loaded.settings),
75
+ ...trusted.settings,
76
+ languages: resolveEnabledLanguages(trusted.settings),
68
77
  };
69
78
  const availability = await getInterpreterAvailability(settings, createInterpreterDetector());
79
+ const pyReason = availability.py.detected.ok ? undefined : availability.py.detected.reason;
80
+ reportSettingsProblems(ctx, [
81
+ ...loaded.warnings,
82
+ ...(trusted.warning === undefined ? [] : [trusted.warning]),
83
+ ...(pyReason === undefined ? [] : [pyReason]),
84
+ ]);
70
85
  const enabledLanguages = enabledLanguagesFrom(settings, availability);
86
+ const jsProcessIsolation = resolveJsIsolation(settings) === "process";
71
87
  const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
72
88
  const activeTools = new Set(pi.getActiveTools());
73
89
  const executeTool = createExecuteTool(pi, activeTools);
@@ -76,6 +92,15 @@ export async function createRuntime(
76
92
  const sessionEnv = { ...sessionEnvironmentFrom(ctx), ...bundledBunPathEntry() };
77
93
  const configuredPoolWidth = settings.parallelPoolWidth;
78
94
  const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
95
+ const pythonEnvironments =
96
+ availability.py.detected.ok && enabledLanguages.py
97
+ ? new PythonEnvironments({
98
+ artifactsDir: artifacts.dir,
99
+ cwd: ctx.cwd,
100
+ interpreter: availability.py.detected.path,
101
+ settings,
102
+ })
103
+ : undefined;
79
104
  const manager = await create({
80
105
  sessionId,
81
106
  ownerSessionId: ctx.sessionManager.getSessionId(),
@@ -88,6 +113,7 @@ export async function createRuntime(
88
113
  listTools: () => pi.getAllTools(),
89
114
  complete,
90
115
  ...(handles === undefined ? {} : { handles }),
116
+ ...(pythonEnvironments === undefined ? {} : { environments: { python: pythonEnvironments } }),
91
117
  });
92
118
  return {
93
119
  sessionId,
@@ -95,7 +121,11 @@ export async function createRuntime(
95
121
  parallelPoolWidth,
96
122
  manager,
97
123
  enabledLanguages,
98
- runtimes: runtimesFromAvailability(availability, jsRuntimeInfo()),
124
+ runtimes: runtimesFromAvailability(
125
+ availability,
126
+ (jsProcessIsolation ? processRuntimeInfo(undefined) : undefined) ??
127
+ jsRuntimeInfo(process.versions, process.execPath, jsProcessIsolation),
128
+ ),
99
129
  settings,
100
130
  artifactsDir: artifacts.dir,
101
131
  executeTool,
@@ -107,19 +137,18 @@ export async function createRuntime(
107
137
  env: { ...process.env, ...sessionEnv },
108
138
  settings,
109
139
  }),
110
- ...(availability.py.detected.ok && enabledLanguages.py
111
- ? {
112
- pythonEnvironments: new PythonEnvironments({
113
- artifactsDir: artifacts.dir,
114
- cwd: ctx.cwd,
115
- interpreter: availability.py.detected.path,
116
- settings,
117
- }),
118
- }
119
- : {}),
140
+ ...(pythonEnvironments === undefined ? {} : { pythonEnvironments }),
120
141
  };
121
142
  }
122
143
 
144
+ /** Settings problems reach the user: a notice with a UI, otherwise stderr (print and RPC runs). */
145
+ function reportSettingsProblems(ctx: ExtensionContext, problems: readonly string[]): void {
146
+ for (const problem of problems) {
147
+ if (ctx.hasUI) ctx.ui.notify(`[senpi-codemode] ${problem}`, "warning");
148
+ else console.error(`[senpi-codemode] ${problem}`);
149
+ }
150
+ }
151
+
123
152
  export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
124
153
  // A cell names tools directly and cannot run tool_search first, so eval opts in to
125
154
  // lazy activation. Eligibility still belongs to the extension that registered the tool.
@@ -10,10 +10,14 @@ export interface JsRuntimeVersions {
10
10
  export function jsRuntimeInfo(
11
11
  versions: JsRuntimeVersions = process.versions,
12
12
  execPath: string = process.execPath,
13
+ processIsolation = false,
13
14
  ): EvalRuntimeInfo {
14
15
  const bun = versions.bun;
15
- if (bun !== undefined && bun.length > 0) return { name: "bun", version: bun, path: execPath };
16
- return { name: "node", version: versions.node, path: execPath };
16
+ const base =
17
+ bun !== undefined && bun.length > 0
18
+ ? { name: "bun", version: bun, path: execPath }
19
+ : { name: "node", version: versions.node, path: execPath };
20
+ return processIsolation ? { ...base, isolation: "process" } : base;
17
21
  }
18
22
 
19
23
  /** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */
@@ -1,3 +1,4 @@
1
+ import { statSync } from "node:fs";
1
2
  import { stat } from "node:fs/promises";
2
3
 
3
4
  // Only these codes mean the directory is gone or was never one; anything else (EACCES, ELOOP,
@@ -21,15 +22,30 @@ export async function assertSessionCwdAvailable(cwd: string): Promise<void> {
21
22
  try {
22
23
  isDirectory = (await stat(cwd)).isDirectory();
23
24
  } catch (error) {
24
- if (
25
- error instanceof Error &&
26
- "code" in error &&
27
- typeof error.code === "string" &&
28
- MISSING_DIRECTORY_CODES.has(error.code)
29
- ) {
30
- throw new CodemodeSessionCwdUnavailableError(cwd, error.code);
31
- }
32
- throw error;
25
+ throw sessionCwdStatError(cwd, error);
33
26
  }
34
27
  if (!isDirectory) throw new CodemodeSessionCwdUnavailableError(cwd, "not a directory");
35
28
  }
29
+
30
+ /** Warm reuse checks every cell without a filesystem thread-pool round trip. */
31
+ export function assertSessionCwdAvailableSync(cwd: string): void {
32
+ let isDirectory: boolean;
33
+ try {
34
+ isDirectory = statSync(cwd).isDirectory();
35
+ } catch (error) {
36
+ throw sessionCwdStatError(cwd, error);
37
+ }
38
+ if (!isDirectory) throw new CodemodeSessionCwdUnavailableError(cwd, "not a directory");
39
+ }
40
+
41
+ function sessionCwdStatError(cwd: string, error: unknown): unknown {
42
+ if (
43
+ error instanceof Error &&
44
+ "code" in error &&
45
+ typeof error.code === "string" &&
46
+ MISSING_DIRECTORY_CODES.has(error.code)
47
+ ) {
48
+ return new CodemodeSessionCwdUnavailableError(cwd, error.code);
49
+ }
50
+ return error;
51
+ }
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@code-yeongyu/senpi";
2
2
  import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
3
3
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
4
4
  import type { CodemodeSettings } from "../config/settings.ts";
5
+ import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
5
6
  import type { HandleRegistry } from "../handles/handle-registry.ts";
6
7
  import type { InterpreterAvailability } from "../interpreters/detect.ts";
7
8
  import type { SessionEnvironment } from "../kernels/session-env.ts";
@@ -42,4 +43,6 @@ export interface CreateCodemodeSessionManagerOptions {
42
43
  readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
43
44
  /** The session generation's handle registry; subprocess kernels reach `wait()`/`handle()` through the bridge. */
44
45
  readonly handles?: HandleRegistry;
46
+ /** The Python environment `packages.install("pip", ...)` reaches from a Python cell over the bridge. */
47
+ readonly environments?: PackagesInstallEnvironments;
45
48
  }
@@ -1,12 +1,15 @@
1
1
  import { join } from "node:path";
2
2
  import type { ExtensionContext } from "@code-yeongyu/senpi";
3
+ import { readProcessFootprint } from "@code-yeongyu/senpi";
3
4
  import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
4
5
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
5
6
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
7
+ import { resolveJsIsolation, resolveKernelToolsEnabled } from "../config/feature-settings.ts";
6
8
  import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
7
9
  import { collectOrphanedChildren } from "../host-sdk.ts";
8
10
  import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
9
11
  import type { KernelToolsCapability, KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
12
+ import type { SessionEnvironment } from "../kernels/session-env.ts";
10
13
  import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
11
14
  import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
12
15
  import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
@@ -21,7 +24,7 @@ import {
21
24
  } from "./kernel-registration.ts";
22
25
  import type { RegisteredKernelSource } from "./kernel-registry.ts";
23
26
  import { ReplaceableKernel } from "./kernel-replacement.ts";
24
- import { assertSessionCwdAvailable } from "./session-cwd.ts";
27
+ import { assertSessionCwdAvailable, assertSessionCwdAvailableSync } from "./session-cwd.ts";
25
28
  import type {
26
29
  BridgeEndpoint,
27
30
  CodemodeSessionManager,
@@ -111,7 +114,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
111
114
  this.#onMessageRefs.set(language, onMessage);
112
115
  const existing = this.#kernels.get(language);
113
116
  if (existing) {
114
- await assertSessionCwdAvailable(this.#options.cwd);
117
+ assertSessionCwdAvailableSync(this.#options.cwd);
115
118
  return existing;
116
119
  }
117
120
  const pending = this.#kernelCreations.get(language);
@@ -252,6 +255,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
252
255
  this.#options.localRoots ??
253
256
  (this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
254
257
  if (language === "js") {
258
+ const memoryThresholds = resolveKernelMemoryThresholds(this.#options.settings.memory);
259
+ const processIsolation = resolveJsIsolation(this.#options.settings) === "process";
255
260
  const kernel = new JavaScriptKernel({
256
261
  sessionId: this.#options.sessionId,
257
262
  cwd: this.#options.cwd,
@@ -259,8 +264,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
259
264
  onMessage,
260
265
  hostToolNames: () => this.#options.listTools?.().map((tool) => tool.name) ?? [],
261
266
  foreignLanguageNames: () => this.#foreignKernelToolNames(),
262
- memory: resolveKernelMemoryThresholds(this.#options.settings.memory),
267
+ memory: memoryThresholds,
263
268
  collectOrphanedChildren,
269
+ ...(processIsolation ? { isolation: "process" as const } : {}),
270
+ ...(processIsolation && memoryThresholds !== undefined
271
+ ? { processMemory: { thresholds: memoryThresholds, readFootprint: readProcessFootprint } }
272
+ : {}),
273
+ ...(resolveKernelToolsEnabled(this.#options.settings) ? {} : { kernelToolsEnabled: false }),
264
274
  ...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
265
275
  ...(localRoots ? { localRoots: { ...localRoots } } : {}),
266
276
  ...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
@@ -276,10 +286,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
276
286
  ...(localRoots ? { localRoots: { ...localRoots } } : {}),
277
287
  ...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
278
288
  };
289
+ // The Python prelude reads this when a cell defines an @tool; only a disabled setting adds it.
290
+ const sessionEnv: SessionEnvironment | undefined = resolveKernelToolsEnabled(this.#options.settings)
291
+ ? this.#options.sessionEnv
292
+ : { ...this.#options.sessionEnv, SENPI_CODEMODE_KERNEL_TOOLS: "0" };
279
293
  const shared = {
280
294
  sessionId: this.#options.sessionId,
281
295
  cwd: this.#options.cwd,
282
- ...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
296
+ ...(sessionEnv ? { sessionEnv } : {}),
283
297
  connection,
284
298
  onMessage,
285
299
  ...lifecycle,
@@ -297,8 +311,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
297
311
  /** Only an already running JavaScript kernel can define a colliding name; none is started for this. */
298
312
  #javaScriptKernelToolsDescribe(names: readonly string[]): Promise<KernelToolsDescribeResult> | undefined {
299
313
  const js = this.#kernels.get("js");
300
- if (!js || !("describeKernelTools" in js) || typeof js.describeKernelTools !== "function") return undefined;
301
- return (js.describeKernelTools as (names: readonly string[]) => Promise<KernelToolsDescribeResult>)(names);
314
+ if (!js || typeof js.describeKernelTools !== "function") return undefined;
315
+ return js.describeKernelTools(names);
302
316
  }
303
317
 
304
318
  #foreignKernelToolNames(): string[] {
package/src/index.ts CHANGED
@@ -10,7 +10,8 @@ import {
10
10
  resolveMaxDetachedCells,
11
11
  resolveRunBudgetSeconds,
12
12
  } from "./config/settings.ts";
13
- import { EvalNotifier } from "./extension/eval-notifier.ts";
13
+ import { registerEvalCellsRpc } from "./extension/eval-cells-rpc.ts";
14
+ import { type EvalNotificationContent, EvalNotifier } from "./extension/eval-notifier.ts";
14
15
  import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
15
16
  import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
16
17
  import { hostLine, modelIdFrom } from "./extension/host-facts.ts";
@@ -57,13 +58,16 @@ export interface CodemodeExtensionAPI {
57
58
  getActiveTools(): string[];
58
59
  getAllTools(): readonly EvalSchemaToolInfo[];
59
60
  sendMessage(
60
- message: { customType: string; content: string; display: boolean },
61
+ message: { customType: string; content: EvalNotificationContent; display: boolean },
61
62
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
62
63
  ): void;
63
64
  /** Optional host event bus; a host without one turns extension event emission into a harmless no-op. */
64
65
  events?: { emit(name: string, data: unknown): void };
65
66
  /** Optional host RPC surface for forwarding extension-owned events to connected clients. */
66
- rpc?: { emit(name: string, data: unknown): void };
67
+ rpc?: {
68
+ emit(name: string, data: unknown): void;
69
+ handle?(name: string, handler: (data: unknown) => unknown | Promise<unknown>): void;
70
+ };
67
71
  }
68
72
 
69
73
  export interface SenpiCodemodeOptions {
@@ -94,6 +98,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
94
98
  let activeContext: ExtensionContext | undefined;
95
99
  let activeCells: EvalDetachedCellManager | undefined;
96
100
  let activeHandles: HandleRegistry | undefined;
101
+ registerEvalCellsRpc(pi.rpc, () => activeCells);
97
102
  let promptPreludeDocs = "";
98
103
  const notifier = new EvalNotifier({
99
104
  sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
@@ -20,6 +20,8 @@ export interface InterpreterDetected {
20
20
 
21
21
  export interface InterpreterUnavailable {
22
22
  readonly ok: false;
23
+ /** Why a configured interpreter could not be used; unset when detection simply found none. */
24
+ readonly reason?: string;
23
25
  }
24
26
 
25
27
  export type InterpreterDetection = InterpreterDetected | InterpreterUnavailable;
@@ -43,6 +45,8 @@ export interface CreateInterpreterDetectorOptions {
43
45
 
44
46
  export interface InterpreterDetector {
45
47
  detect(language: CodemodeLanguage): Promise<InterpreterDetection>;
48
+ /** Probes exactly this executable (no PATH candidates, no splitting on spaces). */
49
+ detectExplicit(path: string): Promise<InterpreterDetection>;
46
50
  }
47
51
 
48
52
  export interface LanguageAvailability {
@@ -74,6 +78,9 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
74
78
  cache.set(language, pending);
75
79
  return pending;
76
80
  },
81
+ detectExplicit(path) {
82
+ return probeExplicit(path, probe);
83
+ },
77
84
  };
78
85
  }
79
86
 
@@ -82,7 +89,7 @@ export async function getInterpreterAvailability(
82
89
  detector: InterpreterDetector,
83
90
  ): Promise<InterpreterAvailability> {
84
91
  return {
85
- py: await availabilityFor("py", settings.languages.py, detector),
92
+ py: await pythonAvailability(settings, detector),
86
93
  js: await availabilityFor("js", settings.languages.js, detector),
87
94
  rb: await availabilityFor("rb", settings.languages.rb, detector),
88
95
  jl: await availabilityFor("jl", settings.languages.jl, detector),
@@ -100,6 +107,32 @@ async function availabilityFor(
100
107
  };
101
108
  }
102
109
 
110
+ /** `languages.pyInterpreter`, when set, is the only interpreter Python uses; a path that does not answer makes Python unavailable. */
111
+ async function pythonAvailability(
112
+ settings: CodemodeSettings,
113
+ detector: InterpreterDetector,
114
+ ): Promise<LanguageAvailability> {
115
+ const configured = settings.languages.pyInterpreter;
116
+ if (configured === undefined || !settings.languages.py)
117
+ return await availabilityFor("py", settings.languages.py, detector);
118
+ return { enabled: true, detected: await detector.detectExplicit(configured) };
119
+ }
120
+
121
+ async function probeExplicit(path: string, probe: ExecFileProbe): Promise<InterpreterDetection> {
122
+ const reason = (detail: string): InterpreterUnavailable => ({
123
+ ok: false,
124
+ reason: `languages.pyInterpreter "${path}" ${detail}; Python is unavailable in this session`,
125
+ });
126
+ try {
127
+ const result = await probe(path, ["--version"], { timeoutMs: probeTimeoutMs });
128
+ const version = parseVersion(`${result.stdout}\n${result.stderr}`);
129
+ if (version === null) return reason("did not report a Python version");
130
+ return { ok: true, path, version, resolvedPath: path };
131
+ } catch (error) {
132
+ return reason(`could not run (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`);
133
+ }
134
+ }
135
+
103
136
  async function detectUncached(
104
137
  language: CodemodeLanguage,
105
138
  hostPlatform: NodeJS.Platform,
@@ -16,6 +16,8 @@ runner/prelude assets).
16
16
  | Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
17
17
  | Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
18
18
  | Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
19
+ | JS process isolation | `js/process-worker.ts` (host side), `js/process-entry.js` (child entry), `js/kernel-memory.ts`, `js/control-frames.ts` |
20
+ | Sandbox cells | `sandbox/sandbox-cell.ts` (host executor); `sandbox/vendor/pi-codemode/**` (vendored runtime, see its `VENDORED.md`) |
19
21
 
20
22
  ## CONVENTIONS
21
23
 
@@ -0,0 +1,128 @@
1
+ const SENPI_MEMORY_INTERNALS = Set([:senpi_current_cell, :senpi_connection, :senpi_frame_io, :senpi_stdout_capture, :senpi_stderr_capture, :senpi_protocol_stdin])
2
+
3
+ const SENPI_SIZER_SAMPLE = 1_000
4
+ const SENPI_SIZER_NODE_BUDGET = 5_000
5
+ const SENPI_SIZER_MAX_DEPTH = 64
6
+ const SENPI_SIZER_POINTER = 8
7
+ const SENPI_SIZER_OBJECT = 16
8
+ const SENPI_SIZER_MIN_REPORTED = 1024 * 1024
9
+
10
+ # Sizes a global with sampling and a node budget per global (leaves count too), so one deep global
11
+ # never hides the ones measured after it and a huge container costs a bounded walk. Only concrete Base
12
+ # containers are iterated; any other AbstractDict or AbstractSet is sized as an opaque struct, so no user
13
+ # length or iterate method runs during a memory report.
14
+ mutable struct SenpiSizer
15
+ seen::IdDict{Any, Nothing}
16
+ nodes::Int
17
+ approximate::Bool
18
+ end
19
+ SenpiSizer() = SenpiSizer(IdDict{Any, Nothing}(), 0, false)
20
+
21
+ senpi_over_budget(sizer::SenpiSizer) = sizer.nodes >= SENPI_SIZER_NODE_BUDGET
22
+
23
+ function senpi_size(sizer::SenpiSizer, value, depth::Int)::Int
24
+ sizer.nodes += 1
25
+ value isa Type && return 0
26
+ value isa Union{Number, Char, Bool, Symbol, Nothing} && return isbits(value) ? sizeof(value) : 0
27
+ value isa String && return SENPI_SIZER_OBJECT + sizeof(value)
28
+ if ismutable(value)
29
+ haskey(sizer.seen, value) && return 0
30
+ sizer.seen[value] = nothing
31
+ end
32
+ if depth >= SENPI_SIZER_MAX_DEPTH || senpi_over_budget(sizer)
33
+ sizer.approximate = true
34
+ return SENPI_SIZER_OBJECT
35
+ end
36
+ if value isa Array
37
+ isbitstype(eltype(value)) && return SENPI_SIZER_OBJECT + sizeof(value)
38
+ return SENPI_SIZER_OBJECT + length(value) * SENPI_SIZER_POINTER +
39
+ senpi_sampled(sizer, length(value), depth) do index
40
+ isassigned(value, index) ? value[index] : nothing
41
+ end
42
+ elseif value isa Union{Dict, IdDict}
43
+ count = length(value)
44
+ total = 0
45
+ taken = 0
46
+ for (key, item) in value
47
+ senpi_over_budget(sizer) && break
48
+ total += senpi_size(sizer, key, depth + 1) + senpi_size(sizer, item, depth + 1)
49
+ taken += 1
50
+ taken >= SENPI_SIZER_SAMPLE && break
51
+ end
52
+ taken < count && (sizer.approximate = true)
53
+ return SENPI_SIZER_OBJECT + count * 2 * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
54
+ elseif value isa Union{Set, Tuple}
55
+ count = length(value)
56
+ total = 0
57
+ taken = 0
58
+ for item in value
59
+ senpi_over_budget(sizer) && break
60
+ total += senpi_size(sizer, item, depth + 1)
61
+ taken += 1
62
+ taken >= SENPI_SIZER_SAMPLE && break
63
+ end
64
+ taken < count && (sizer.approximate = true)
65
+ return SENPI_SIZER_OBJECT + count * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
66
+ end
67
+ isbits(value) && return sizeof(value)
68
+ fields = fieldcount(typeof(value))
69
+ return SENPI_SIZER_OBJECT + fields * SENPI_SIZER_POINTER +
70
+ senpi_sampled(sizer, fields, depth) do index
71
+ isdefined(value, index) ? getfield(value, index) : nothing
72
+ end
73
+ end
74
+
75
+ # Up to SENPI_SIZER_SAMPLE evenly spaced elements; once the budget runs out part-way, the elements measured
76
+ # so far stand in for the rest.
77
+ function senpi_sampled(at, sizer::SenpiSizer, count::Int, depth::Int)::Int
78
+ count == 0 && return 0
79
+ picks = min(count, SENPI_SIZER_SAMPLE)
80
+ picks < count && (sizer.approximate = true)
81
+ step = count / picks
82
+ total = 0
83
+ measured = 0
84
+ for sample in 1:picks
85
+ if senpi_over_budget(sizer)
86
+ sizer.approximate = true
87
+ break
88
+ end
89
+ total += senpi_size(sizer, at(1 + floor(Int, (sample - 1) * step)), depth + 1)
90
+ measured += 1
91
+ end
92
+ return measured == 0 ? 0 : round(Int, total / measured * count)
93
+ end
94
+
95
+ function senpi_largest_globals(limit::Int)
96
+ try
97
+ sizer = SenpiSizer()
98
+ candidates = Tuple{String, Int, Bool}[]
99
+ Base.invokelatest() do
100
+ for name in names(Main, all = true)
101
+ name in SENPI_MEMORY_INTERNALS && continue
102
+ name in (:Main, :Base, :Core, :Ans, :ans) && continue
103
+ lowered = lowercase(string(name))
104
+ startswith(lowered, "senpi_") && continue
105
+ startswith(string(name), "Senpi") && continue
106
+ startswith(string(name), "#") && continue
107
+ isdefined(Main, name) || continue
108
+ value = getfield(Main, name)
109
+ value isa Module && continue
110
+ value isa IO && continue
111
+ value isa Type && continue
112
+ value isa Function && continue
113
+ sizer.approximate = false
114
+ sizer.nodes = 0
115
+ bytes = try
116
+ senpi_size(sizer, value, 0) + SENPI_SIZER_POINTER
117
+ catch
118
+ 0
119
+ end
120
+ bytes >= SENPI_SIZER_MIN_REPORTED && push!(candidates, (string(name), bytes, sizer.approximate))
121
+ end
122
+ end
123
+ sort!(candidates, by = entry -> entry[2], rev = true)
124
+ [approximate ? Dict{String, Any}("name" => name, "bytes" => bytes, "approximate" => true) : Dict{String, Any}("name" => name, "bytes" => bytes) for (name, bytes, approximate) in candidates[1:min(limit, end)]]
125
+ catch
126
+ Dict{String, Any}[]
127
+ end
128
+ end
@@ -231,12 +231,25 @@ function workpool(agent::AbstractDict, name::AbstractString; mode=nothing, tools
231
231
  throw(SenpiBridgeError("Host did not return a workpool identity", "workpool_unavailable"))
232
232
  end
233
233
 
234
- include("handles.jl")
234
+ const SENPI_HANDLES_INCLUDED = Ref(false)
235
+ const SENPI_HANDLES_INCLUDE_LOCK = ReentrantLock()
236
+
237
+ function senpi_ensure_handles()
238
+ lock(SENPI_HANDLES_INCLUDE_LOCK) do
239
+ if !SENPI_HANDLES_INCLUDED[]
240
+ include(joinpath(@__DIR__, "handles.jl"))
241
+ SENPI_HANDLES_INCLUDED[] = true
242
+ end
243
+ end
244
+ nothing
245
+ end
235
246
 
236
247
  # Rich view of an agent record, workpool, completion handle or saved reference; see tool_schema("eval:helpers").
237
248
  # The barrier is `Base.wait` on the view types (never a `Main.wait`): wait(handle(node)) or wait([handle(a), handle(b)]).
238
249
  function handle(value)
239
- senpi_handle_view(value)
250
+ senpi_ensure_handles()
251
+ # Julia 1.12 ages global bindings too: resolve the new helper inside the latest world.
252
+ Base.invokelatest(() -> senpi_handle_view(value))
240
253
  end
241
254
 
242
255
  function completion(prompt::AbstractString; model="default", system=nothing, schema=nothing, kwargs...)