@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.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 (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -116,9 +116,13 @@ export async function waitForPromiseWithCancellation<T>(
116
116
  promise: Promise<T>,
117
117
  options: { signal?: AbortSignal; deadlineMs?: number },
118
118
  cancelledErrorClass: CancelledErrorClass,
119
+ timedOutResolver?: (error: unknown, signal?: AbortSignal) => boolean,
119
120
  ): Promise<T> {
120
121
  if (options.signal?.aborted) {
121
- throw new cancelledErrorClass(isTimedOutCancellation(options.signal.reason, cancelledErrorClass, options.signal));
122
+ throw new cancelledErrorClass(
123
+ timedOutResolver?.(options.signal.reason, options.signal) ??
124
+ isTimedOutCancellation(options.signal.reason, cancelledErrorClass, options.signal),
125
+ );
122
126
  }
123
127
  const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
124
128
  if (remainingMs !== undefined && remainingMs <= 0) {
@@ -139,7 +143,8 @@ export async function waitForPromiseWithCancellation<T>(
139
143
  finish(() =>
140
144
  reject(
141
145
  new cancelledErrorClass(
142
- isTimedOutCancellation(options.signal?.reason, cancelledErrorClass, options.signal),
146
+ timedOutResolver?.(options.signal?.reason, options.signal) ??
147
+ isTimedOutCancellation(options.signal?.reason, cancelledErrorClass, options.signal),
143
148
  ),
144
149
  ),
145
150
  );
@@ -276,9 +281,32 @@ interface ManagedKernelEnvOptions {
276
281
  bridge?: { url: string; token: string };
277
282
  localRoots?: Record<string, string>;
278
283
  }
284
+ interface ManagedKernelEnvPolicy {
285
+ sparse?: boolean;
286
+ }
279
287
 
280
- export function buildManagedKernelEnvPatch(options: ManagedKernelEnvOptions): Record<string, string | null> {
288
+ export function buildManagedKernelEnvPatch(options: ManagedKernelEnvOptions): Record<string, string | null>;
289
+ export function buildManagedKernelEnvPatch(
290
+ options: ManagedKernelEnvOptions,
291
+ policy: { sparse: true },
292
+ ): Record<string, string | undefined>;
293
+ export function buildManagedKernelEnvPatch(
294
+ options: ManagedKernelEnvOptions,
295
+ policy?: ManagedKernelEnvPolicy,
296
+ ): KernelEnvPatch {
281
297
  const localRoots = options.localRoots;
298
+ if (policy?.sparse) {
299
+ const patch: Record<string, string | undefined> = {};
300
+ if (options.sessionFile) patch.PI_SESSION_FILE = options.sessionFile;
301
+ if (options.artifactsDir) patch.PI_ARTIFACTS_DIR = options.artifactsDir;
302
+ if (options.bridge) {
303
+ patch.PI_TOOL_BRIDGE_URL = options.bridge.url;
304
+ patch.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
305
+ patch.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId ?? "";
306
+ }
307
+ if (localRoots) patch.PI_EVAL_LOCAL_ROOTS = JSON.stringify(localRoots);
308
+ return patch;
309
+ }
282
310
  return {
283
311
  PI_SESSION_FILE: options.sessionFile ?? null,
284
312
  PI_ARTIFACTS_DIR: options.artifactsDir ?? null,
@@ -289,13 +317,18 @@ export function buildManagedKernelEnvPatch(options: ManagedKernelEnvOptions): Re
289
317
  };
290
318
  }
291
319
 
292
- export function buildManagedKernelEnv(options: ManagedKernelEnvOptions): Record<string, string> | undefined {
293
- const patch = buildManagedKernelEnvPatch(options);
320
+ export function buildManagedKernelEnv(
321
+ options: ManagedKernelEnvOptions,
322
+ policy?: ManagedKernelEnvPolicy,
323
+ ): Record<string, string> | undefined {
324
+ const patch = policy?.sparse
325
+ ? buildManagedKernelEnvPatch(options, { sparse: true })
326
+ : buildManagedKernelEnvPatch(options);
294
327
  const env: Record<string, string> = {};
295
328
  let hasKeys = false;
296
329
  for (const key of MANAGED_KERNEL_ENV_KEYS) {
297
330
  const value = patch[key];
298
- if (value !== null) {
331
+ if (typeof value === "string") {
299
332
  env[key] = value;
300
333
  hasKeys = true;
301
334
  }
@@ -2,11 +2,21 @@ import * as path from "node:path";
2
2
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
3
3
  import type { ToolSession } from "../../tools";
4
4
  import {
5
- attachSessionOwner,
5
+ buildManagedKernelEnv,
6
+ buildManagedKernelEnvPatch,
6
7
  createCancelledKernelResult,
7
8
  executeWithKernelBase,
8
- resolveOwnerScopedSessionKey,
9
+ getExecutionDeadlineMs,
10
+ isCancellationError,
11
+ isTimedOutCancellation,
12
+ waitForPromiseWithCancellation,
9
13
  } from "../executor-base";
14
+ import {
15
+ createKernelSessionRegistry,
16
+ type KernelSession,
17
+ normalizeKernelSessionCwd,
18
+ requireRemainingKernelTimeoutMs,
19
+ } from "../kernel-session-registry";
10
20
  import { ensurePyToolBridge, type PyToolBridgeInfo } from "../py/tool-bridge";
11
21
  import type { EvalDisplayOutput, EvalStatusEvent } from "../types";
12
22
  import {
@@ -58,22 +68,6 @@ export interface JuliaResult {
58
68
  stdinRequested: boolean;
59
69
  }
60
70
 
61
- interface JuliaSessionOwners {
62
- ownerIds: Set<string>;
63
- hasFallbackOwner: boolean;
64
- }
65
-
66
- interface JuliaSession extends JuliaSessionOwners {
67
- sessionKey: string;
68
- sessionId: string;
69
- cwd: string;
70
- kernel: JuliaKernel;
71
- }
72
-
73
- interface StartingJuliaSession extends JuliaSessionOwners {
74
- promise: Promise<JuliaSession>;
75
- }
76
-
77
71
  class JuliaExecutionCancelledError extends Error {
78
72
  constructor(readonly timedOut: boolean) {
79
73
  super(timedOut ? "Julia execution timed out" : "Julia execution cancelled");
@@ -81,14 +75,6 @@ class JuliaExecutionCancelledError extends Error {
81
75
  }
82
76
  }
83
77
 
84
- const sessions = new Map<string, JuliaSession>();
85
- const startingSessions = new Map<string, StartingJuliaSession>();
86
- const resettingSessions = new Map<string, Promise<void>>();
87
-
88
- function normalizeSessionCwd(cwd: string): string {
89
- return path.resolve(cwd);
90
- }
91
-
92
78
  function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefined): string {
93
79
  if (interpreter === undefined) return "";
94
80
  const resolved = resolveExplicitJuliaRuntime(interpreter, cwd, {}).juliaPath;
@@ -99,85 +85,39 @@ function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefin
99
85
  }
100
86
  }
101
87
 
102
- function buildSessionKey(sessionId: string, cwd: string, interpreter: string | undefined): string {
103
- const normalizedCwd = normalizeSessionCwd(cwd);
104
- const normalizedInterpreter = normalizeExplicitInterpreter(normalizedCwd, interpreter);
105
- return `${sessionId}::${normalizedCwd}::${normalizedInterpreter}`;
106
- }
107
-
108
- function isCancellationError(error: unknown): boolean {
109
- if (error instanceof JuliaExecutionCancelledError) return true;
110
- if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return true;
111
- if (
112
- error &&
113
- typeof error === "object" &&
114
- "name" in error &&
115
- (error.name === "AbortError" || error.name === "TimeoutError")
116
- )
117
- return true;
118
- return false;
119
- }
120
-
121
- function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
122
- if (error instanceof JuliaExecutionCancelledError) return error.timedOut;
123
- if (error instanceof Error && error.name === "TimeoutError") return true;
124
- if (error && typeof error === "object" && "name" in error && error.name === "TimeoutError") return true;
125
- if (signal?.reason instanceof Error && signal.reason.name === "TimeoutError") return true;
126
- return false;
127
- }
128
-
129
- function getExecutionDeadlineMs(options?: Pick<JuliaExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
130
- if (options?.deadlineMs !== undefined) return options.deadlineMs;
131
- if (options?.timeoutMs !== undefined && options.timeoutMs > 0) return Date.now() + options.timeoutMs;
132
- return undefined;
133
- }
134
-
135
- function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
136
- if (deadlineMs === undefined) return undefined;
137
- return Math.max(0, deadlineMs - Date.now());
88
+ function isJuliaCancellationError(error: unknown): boolean {
89
+ return (
90
+ isCancellationError(error, JuliaExecutionCancelledError) ||
91
+ (!!error &&
92
+ typeof error === "object" &&
93
+ "name" in error &&
94
+ (error.name === "AbortError" || error.name === "TimeoutError"))
95
+ );
138
96
  }
139
97
 
140
- function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
141
- if (deadlineMs === undefined) return undefined;
142
- const remaining = getRemainingTimeoutMs(deadlineMs);
143
- if (remaining !== undefined && remaining <= 0) {
144
- throw new JuliaExecutionCancelledError(true);
145
- }
146
- return remaining;
98
+ function isTimedOutJuliaCancellation(error: unknown, signal?: AbortSignal): boolean {
99
+ return (
100
+ isTimedOutCancellation(error, JuliaExecutionCancelledError, signal) ||
101
+ (!!error && typeof error === "object" && "name" in error && error.name === "TimeoutError")
102
+ );
147
103
  }
148
104
 
149
- async function waitForPromiseWithCancellation<T>(
105
+ async function waitForJuliaPromise<T>(
150
106
  promise: Promise<T>,
151
107
  options: Pick<JuliaExecutorOptions, "signal" | "deadlineMs">,
152
108
  ): Promise<T> {
153
- if (options.signal?.aborted) {
154
- throw new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
155
- }
156
- const cleanups: Array<() => void> = [];
157
- const { promise: cancelPromise, reject } = Promise.withResolvers<never>();
158
-
159
- if (options.signal) {
160
- const onAbort = () => {
161
- reject(new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal?.reason, options.signal)));
162
- };
163
- options.signal.addEventListener("abort", onAbort, { once: true });
164
- cleanups.push(() => options.signal?.removeEventListener("abort", onAbort));
165
- }
166
-
167
- const deadlineMs = options.deadlineMs;
168
- if (typeof deadlineMs === "number" && deadlineMs > Date.now()) {
169
- const timeout = setTimeout(() => {
170
- reject(new JuliaExecutionCancelledError(true));
171
- }, deadlineMs - Date.now());
172
- timeout.unref?.();
173
- cleanups.push(() => clearTimeout(timeout));
174
- }
109
+ const deadlineMs =
110
+ typeof options.deadlineMs === "number" && options.deadlineMs > Date.now() ? options.deadlineMs : undefined;
111
+ return await waitForPromiseWithCancellation(
112
+ promise,
113
+ { ...options, deadlineMs },
114
+ JuliaExecutionCancelledError,
115
+ isTimedOutJuliaCancellation,
116
+ );
117
+ }
175
118
 
176
- try {
177
- return await Promise.race([promise, cancelPromise]);
178
- } finally {
179
- for (const cleanup of cleanups) cleanup();
180
- }
119
+ function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
120
+ return requireRemainingKernelTimeoutMs(deadlineMs, JuliaExecutionCancelledError);
181
121
  }
182
122
 
183
123
  function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
@@ -198,55 +138,9 @@ function createCancelledJuliaResult(_timedOut: boolean, timeoutMs?: number): Jul
198
138
  return createCancelledKernelResult(output);
199
139
  }
200
140
 
201
- function buildKernelEnvPatch(options: {
202
- sessionFile?: string;
203
- artifactsDir?: string;
204
- bridge?: PyToolBridgeInfo;
205
- bridgeSessionId?: string;
206
- localRoots?: Record<string, string>;
207
- }): Record<string, string | undefined> {
208
- const patch: Record<string, string | undefined> = {};
209
- if (options.sessionFile) patch.PI_SESSION_FILE = options.sessionFile;
210
- if (options.artifactsDir) patch.PI_ARTIFACTS_DIR = options.artifactsDir;
211
- if (options.bridge) {
212
- patch.PI_TOOL_BRIDGE_URL = options.bridge.url;
213
- patch.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
214
- patch.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId ?? "";
215
- }
216
- if (options.localRoots) {
217
- patch.PI_EVAL_LOCAL_ROOTS = JSON.stringify(options.localRoots);
218
- }
219
- return patch;
220
- }
221
-
222
- function buildKernelEnv(options: {
223
- sessionFile?: string;
224
- artifactsDir?: string;
225
- bridge?: PyToolBridgeInfo;
226
- bridgeSessionId?: string;
227
- localRoots?: Record<string, string>;
228
- }): Record<string, string> | undefined {
229
- const patch = buildKernelEnvPatch(options);
230
- const keys = Object.keys(patch);
231
- if (keys.length === 0) return undefined;
232
- const realEnv: Record<string, string> = {};
233
- for (const key in patch) {
234
- const val = patch[key];
235
- if (typeof val === "string") realEnv[key] = val;
236
- }
237
- return realEnv;
238
- }
239
-
240
141
  async function startKernel(cwd: string, options: JuliaExecutorOptions): Promise<JuliaKernel> {
241
142
  requireRemainingTimeoutMs(options.deadlineMs);
242
- const env: Record<string, string | undefined> = {};
243
- const patch = buildKernelEnv(options);
244
- if (patch) {
245
- for (const key in patch) {
246
- const value = patch[key];
247
- if (typeof value === "string") env[key] = value;
248
- }
249
- }
143
+ const env: Record<string, string | undefined> = buildManagedKernelEnv(options, { sparse: true }) ?? {};
250
144
  return await JuliaKernel.start({
251
145
  cwd,
252
146
  interpreter: options.interpreter,
@@ -256,163 +150,6 @@ async function startKernel(cwd: string, options: JuliaExecutorOptions): Promise<
256
150
  });
257
151
  }
258
152
 
259
- async function acquireSession(
260
- sessionKey: string,
261
- sessionId: string,
262
- cwd: string,
263
- options: JuliaExecutorOptions,
264
- ): Promise<JuliaSession> {
265
- const existing = sessions.get(sessionKey);
266
- if (existing) {
267
- attachSessionOwner(existing, sessionId, options.kernelOwnerId);
268
- return existing;
269
- }
270
-
271
- const inFlight = startingSessions.get(sessionKey);
272
- if (inFlight) {
273
- attachSessionOwner(inFlight, sessionId, options.kernelOwnerId);
274
- return await waitForPromiseWithCancellation(inFlight.promise, options);
275
- }
276
-
277
- let startingSession!: StartingJuliaSession;
278
- const startPromise = (async () => {
279
- const kernel = await startKernel(cwd, options);
280
- const session: JuliaSession = {
281
- sessionKey,
282
- sessionId,
283
- cwd,
284
- kernel,
285
- ownerIds: new Set(startingSession.ownerIds),
286
- hasFallbackOwner: startingSession.hasFallbackOwner,
287
- };
288
- if (startingSessions.get(sessionKey) === startingSession) {
289
- sessions.set(sessionKey, session);
290
- }
291
- return session;
292
- })();
293
-
294
- startingSession = {
295
- ownerIds: new Set(),
296
- hasFallbackOwner: false,
297
- promise: startPromise,
298
- };
299
- attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
300
- startingSessions.set(sessionKey, startingSession);
301
- try {
302
- return await waitForPromiseWithCancellation(startPromise, options);
303
- } finally {
304
- if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
305
- }
306
- }
307
-
308
- async function replaceSessionKernel(session: JuliaSession, cwd: string, options: JuliaExecutorOptions): Promise<void> {
309
- logger.warn("Julia subprocess died or is unresponsive; spawning fresh process", {
310
- sessionKey: session.sessionKey,
311
- });
312
- const oldKernel = session.kernel;
313
- const remaining = getRemainingTimeoutMs(options.deadlineMs);
314
- await oldKernel
315
- .shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
316
- .catch(() => undefined);
317
- if (sessions.get(session.sessionKey) !== session) {
318
- throw new JuliaExecutionCancelledError(false);
319
- }
320
- requireRemainingTimeoutMs(options.deadlineMs);
321
- const nextKernel = await startKernel(cwd, options);
322
- if (sessions.get(session.sessionKey) !== session) {
323
- await nextKernel.shutdown().catch(() => undefined);
324
- throw new JuliaExecutionCancelledError(false);
325
- }
326
- session.kernel = nextKernel;
327
- }
328
-
329
- async function resetSession(sessionKey: string): Promise<void> {
330
- const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
331
- if (!session) return;
332
- sessions.delete(sessionKey);
333
- await session.kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }).catch(() => undefined);
334
- }
335
-
336
- export async function disposeAllJuliaKernelSessions(): Promise<void> {
337
- const pending = [...startingSessions.values()].map(starting => starting.promise);
338
- startingSessions.clear();
339
- resettingSessions.clear();
340
- const started = await Promise.allSettled(pending);
341
- const all = [...sessions.entries()];
342
- for (const result of started) {
343
- if (result.status !== "fulfilled") continue;
344
- if (!all.some(([, session]) => session === result.value)) {
345
- all.push([result.value.sessionKey, result.value]);
346
- }
347
- }
348
- for (const [id, session] of all) {
349
- if (sessions.get(id) === session) sessions.delete(id);
350
- }
351
- const results = await Promise.allSettled(all.map(([, session]) => session.kernel.shutdown()));
352
- for (let i = 0; i < all.length; i += 1) {
353
- const [id, session] = all[i];
354
- const result = results[i];
355
- if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
356
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
357
- logger.warn("Julia kernel shutdown not confirmed", {
358
- sessionId: session.sessionId,
359
- sessionKey: id,
360
- cwd: session.cwd,
361
- reason,
362
- });
363
- if (!sessions.has(id)) sessions.set(id, session);
364
- }
365
- }
366
-
367
- export async function disposeJuliaKernelSessionsByOwner(ownerId: string): Promise<void> {
368
- const toShutdown: JuliaSession[] = [];
369
- const startingToShutdown: StartingJuliaSession[] = [];
370
- for (const session of [...sessions.values()]) {
371
- if (!session.ownerIds.has(ownerId)) continue;
372
- if (session.ownerIds.size === 1) {
373
- toShutdown.push(session);
374
- continue;
375
- }
376
- session.ownerIds.delete(ownerId);
377
- }
378
- for (const [sessionKey, starting] of [...startingSessions.entries()]) {
379
- if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
380
- if (starting.ownerIds.size === 1) {
381
- startingSessions.delete(sessionKey);
382
- startingToShutdown.push(starting);
383
- continue;
384
- }
385
- starting.ownerIds.delete(ownerId);
386
- }
387
- for (const session of toShutdown) {
388
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
389
- }
390
- const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
391
- for (const result of started) {
392
- if (result.status !== "fulfilled") continue;
393
- const session = result.value;
394
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
395
- toShutdown.push(session);
396
- }
397
- const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
398
- for (let i = 0; i < toShutdown.length; i += 1) {
399
- const session = toShutdown[i];
400
- const result = results[i];
401
- if (result.status === "fulfilled" && result.value?.confirmed !== false) {
402
- session.ownerIds.delete(ownerId);
403
- continue;
404
- }
405
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
406
- logger.warn("Julia kernel shutdown not confirmed", {
407
- sessionId: session.sessionId,
408
- sessionKey: session.sessionKey,
409
- cwd: session.cwd,
410
- reason,
411
- });
412
- if (!sessions.has(session.sessionKey)) sessions.set(session.sessionKey, session);
413
- }
414
- }
415
-
416
153
  async function executeWithKernel(
417
154
  kernel: JuliaKernel,
418
155
  code: string,
@@ -426,7 +163,7 @@ async function executeWithKernel(
426
163
  errorLogLabel: "Julia",
427
164
  isJulia: true,
428
165
  cancelledErrorClass: JuliaExecutionCancelledError,
429
- buildKernelEnvPatch,
166
+ buildKernelEnvPatch: opts => buildManagedKernelEnvPatch(opts, { sparse: true }),
430
167
  formatKernelTimeoutAnnotation,
431
168
  formatTimeoutAnnotation,
432
169
  resolveDeadlineMs: opts => opts?.deadlineMs,
@@ -434,10 +171,7 @@ async function executeWithKernel(
434
171
  }
435
172
 
436
173
  async function ensureKernelAvailable(cwd: string, options: JuliaExecutorOptions): Promise<void> {
437
- const availability = await waitForPromiseWithCancellation(
438
- checkJuliaKernelAvailability(cwd, options.interpreter),
439
- options,
440
- );
174
+ const availability = await waitForJuliaPromise(checkJuliaKernelAvailability(cwd, options.interpreter), options);
441
175
  if (!availability.ok) {
442
176
  throw new Error(availability.reason ?? "Julia kernel unavailable");
443
177
  }
@@ -454,65 +188,37 @@ async function ensureToolBridge(options: JuliaExecutorOptions): Promise<void> {
454
188
  }
455
189
  }
456
190
 
457
- async function executeOnSession(code: string, cwd: string, options: JuliaExecutorOptions): Promise<JuliaResult> {
458
- const sessionId = options.sessionId ?? `session:${cwd}`;
459
- const sessionKey = resolveOwnerScopedSessionKey({
460
- baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
461
- ownerId: options.kernelOwnerId,
462
- reset: options.reset === true,
463
- hasSession: key => sessions.has(key) || startingSessions.has(key),
464
- getOwners: key => sessions.get(key) ?? startingSessions.get(key),
465
- });
466
- if (options.bridge && !options.bridgeSessionId) {
467
- options.bridgeSessionId = sessionId;
468
- }
469
- if (options.reset) {
470
- const inFlight = resettingSessions.get(sessionKey);
471
- if (inFlight) await inFlight.catch(() => undefined);
472
- else {
473
- const resetPromise = resetSession(sessionKey);
474
- resettingSessions.set(
475
- sessionKey,
476
- resetPromise.then(() => undefined),
477
- );
478
- try {
479
- await resetPromise;
480
- } finally {
481
- resettingSessions.delete(sessionKey);
482
- }
483
- }
484
- } else {
485
- const inFlight = resettingSessions.get(sessionKey);
486
- if (inFlight) await inFlight.catch(() => undefined);
487
- }
488
- const session = await acquireSession(sessionKey, sessionId, cwd, options);
489
- if (options.signal?.aborted) {
490
- throw new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
491
- }
492
- if (sessions.get(session.sessionKey) !== session) {
493
- throw new JuliaExecutionCancelledError(false);
494
- }
495
- if (!session.kernel.isAlive()) {
496
- await replaceSessionKernel(session, cwd, options);
497
- if (sessions.get(session.sessionKey) !== session) {
498
- throw new JuliaExecutionCancelledError(false);
499
- }
500
- }
501
- const runOptions = { ...options, cwd };
502
- try {
503
- return await executeWithKernel(session.kernel, code, runOptions);
504
- } catch (err) {
505
- if (isCancellationError(err) || options.signal?.aborted) throw err;
506
- if (session.kernel.isAlive()) throw err;
507
- if (sessions.get(session.sessionKey) !== session) {
508
- throw new JuliaExecutionCancelledError(false);
509
- }
510
- await replaceSessionKernel(session, cwd, options);
511
- if (sessions.get(session.sessionKey) !== session) {
512
- throw new JuliaExecutionCancelledError(false);
513
- }
514
- return await executeWithKernel(session.kernel, code, runOptions);
515
- }
191
+ const sessionRegistry = createKernelSessionRegistry<
192
+ JuliaKernel,
193
+ JuliaExecutorOptions,
194
+ JuliaResult,
195
+ KernelSession<JuliaKernel>
196
+ >({
197
+ languageLabel: "Julia",
198
+ cancelledErrorClass: JuliaExecutionCancelledError,
199
+ buildSessionKey: (sessionId, cwd, interpreter) => {
200
+ const normalizedCwd = normalizeKernelSessionCwd(cwd);
201
+ const normalizedInterpreter = normalizeExplicitInterpreter(normalizedCwd, interpreter);
202
+ return `${sessionId}::${normalizedCwd}::${normalizedInterpreter}`;
203
+ },
204
+ createSession: session => session,
205
+ startKernel,
206
+ executeWithKernel,
207
+ waitForStartup: waitForJuliaPromise,
208
+ shutdownSession: (session, resetting) =>
209
+ resetting ? session.kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }) : session.kernel.shutdown(),
210
+ clearResetsOnDisposeAll: true,
211
+ logBeforeReplacement: true,
212
+ isCancellation: isJuliaCancellationError,
213
+ isTimedOutCancellation: isTimedOutJuliaCancellation,
214
+ });
215
+
216
+ export async function disposeAllJuliaKernelSessions(): Promise<void> {
217
+ await sessionRegistry.disposeAll();
218
+ }
219
+
220
+ export async function disposeJuliaKernelSessionsByOwner(ownerId: string): Promise<void> {
221
+ await sessionRegistry.disposeByOwner(ownerId);
516
222
  }
517
223
 
518
224
  export async function executeJuliaWithKernel(
@@ -524,8 +230,13 @@ export async function executeJuliaWithKernel(
524
230
  }
525
231
 
526
232
  export async function executeJulia(code: string, options?: JuliaExecutorOptions): Promise<JuliaResult> {
527
- const cwd = normalizeSessionCwd(options?.cwd ?? getProjectDir());
528
- const deadlineMs = getExecutionDeadlineMs(options);
233
+ const cwd = normalizeKernelSessionCwd(options?.cwd ?? getProjectDir());
234
+ const deadlineMs =
235
+ options?.deadlineMs !== undefined
236
+ ? options.deadlineMs
237
+ : options?.timeoutMs !== undefined && options.timeoutMs > 0
238
+ ? getExecutionDeadlineMs(options)
239
+ : undefined;
529
240
  const executionOptions: JuliaExecutorOptions = {
530
241
  ...(options ?? {}),
531
242
  cwd,
@@ -536,15 +247,15 @@ export async function executeJulia(code: string, options?: JuliaExecutorOptions)
536
247
  requireRemainingTimeoutMs(deadlineMs);
537
248
  if (executionOptions.signal?.aborted) {
538
249
  throw new JuliaExecutionCancelledError(
539
- isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
250
+ isTimedOutJuliaCancellation(executionOptions.signal.reason, executionOptions.signal),
540
251
  );
541
252
  }
542
253
  await ensureKernelAvailable(cwd, executionOptions);
543
254
  await ensureToolBridge(executionOptions);
544
- return await executeOnSession(code, cwd, executionOptions);
255
+ return await sessionRegistry.executeOnSession(code, cwd, executionOptions);
545
256
  } catch (err) {
546
- if (isCancellationError(err) || executionOptions.signal?.aborted) {
547
- return createCancelledJuliaResult(isTimedOutCancellation(err, executionOptions.signal));
257
+ if (isJuliaCancellationError(err) || executionOptions.signal?.aborted) {
258
+ return createCancelledJuliaResult(isTimedOutJuliaCancellation(err, executionOptions.signal));
548
259
  }
549
260
  throw err;
550
261
  }