@oh-my-pi/pi-coding-agent 17.2.11 → 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 (152) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
  3. package/dist/cli.js +10795 -10791
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/config/custom-models.d.ts +31 -0
  6. package/dist/types/config/model-config-values.d.ts +19 -0
  7. package/dist/types/config/model-patch.d.ts +93 -0
  8. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  9. package/dist/types/config/model-registry.d.ts +5 -52
  10. package/dist/types/config/settings.d.ts +5 -3
  11. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  12. package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
  13. package/dist/types/discovery/contained-path.d.ts +2 -3
  14. package/dist/types/eval/executor-base.d.ts +8 -2
  15. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  16. package/dist/types/export/share.d.ts +1 -1
  17. package/dist/types/lsp/diagnostics.d.ts +96 -0
  18. package/dist/types/lsp/index.d.ts +7 -128
  19. package/dist/types/lsp/servers.d.ts +72 -0
  20. package/dist/types/lsp/tool.d.ts +42 -0
  21. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  22. package/dist/types/lsp/writethrough.d.ts +33 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +2 -1
  24. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  25. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  26. package/dist/types/modes/theme/color.d.ts +19 -0
  27. package/dist/types/modes/theme/loader.d.ts +19 -0
  28. package/dist/types/modes/theme/schema.d.ts +175 -0
  29. package/dist/types/modes/theme/symbols.d.ts +28 -0
  30. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  31. package/dist/types/modes/theme/theme.d.ts +9 -280
  32. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  33. package/dist/types/registry/agent-registry.d.ts +1 -3
  34. package/dist/types/secrets/index.d.ts +3 -1
  35. package/dist/types/secrets/message-transform.d.ts +40 -0
  36. package/dist/types/secrets/obfuscator.d.ts +0 -108
  37. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  38. package/dist/types/secrets/placeholder.d.ts +94 -0
  39. package/dist/types/secrets/replacement.d.ts +82 -0
  40. package/dist/types/session/agent-session-types.d.ts +6 -0
  41. package/dist/types/session/agent-session.d.ts +1 -1
  42. package/dist/types/session/session-handoff.d.ts +3 -3
  43. package/dist/types/session/session-manager.d.ts +32 -0
  44. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  45. package/dist/types/session/turn-recovery.d.ts +2 -2
  46. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  47. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  48. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  49. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  50. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  51. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  52. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  53. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  54. package/dist/types/tools/gh-common.d.ts +69 -0
  55. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  56. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  57. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  58. package/dist/types/tools/gh-search.d.ts +61 -0
  59. package/dist/types/tools/gh-types.d.ts +340 -0
  60. package/dist/types/tools/gh-view.d.ts +88 -0
  61. package/dist/types/tools/gh.d.ts +4 -201
  62. package/dist/types/tools/read-archive.d.ts +16 -0
  63. package/dist/types/tools/read-format.d.ts +104 -0
  64. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  65. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  66. package/dist/types/tools/read-renderer.d.ts +24 -0
  67. package/dist/types/tools/read-selector.d.ts +27 -0
  68. package/dist/types/tools/read-sqlite.d.ts +16 -0
  69. package/dist/types/tools/read-summary.d.ts +19 -0
  70. package/dist/types/tools/read.d.ts +1 -23
  71. package/dist/types/tools/shell-tokenize.d.ts +2 -1
  72. package/dist/types/utils/changelog.d.ts +0 -5
  73. package/package.json +13 -13
  74. package/src/advisor/runtime.ts +12 -7
  75. package/src/config/custom-models.ts +188 -0
  76. package/src/config/model-config-values.ts +128 -0
  77. package/src/config/model-patch.ts +252 -0
  78. package/src/config/model-provider-discovery.ts +132 -0
  79. package/src/config/model-registry.ts +64 -704
  80. package/src/config/settings.ts +7 -3
  81. package/src/debug/raw-sse-buffer.ts +20 -0
  82. package/src/discovery/agent-plugin-format.ts +7 -7
  83. package/src/discovery/contained-path.ts +2 -5
  84. package/src/edit/hashline/diff.ts +5 -1
  85. package/src/eval/executor-base.ts +39 -6
  86. package/src/eval/jl/executor.ts +82 -371
  87. package/src/eval/kernel-session-registry.ts +398 -0
  88. package/src/eval/py/executor.ts +53 -261
  89. package/src/eval/rb/executor.ts +36 -279
  90. package/src/export/share.ts +2 -1
  91. package/src/lsp/diagnostics.ts +516 -0
  92. package/src/lsp/index.ts +20 -2819
  93. package/src/lsp/servers.ts +296 -0
  94. package/src/lsp/tool.ts +1352 -0
  95. package/src/lsp/workspace-diagnostics.ts +170 -0
  96. package/src/lsp/writethrough.ts +561 -0
  97. package/src/mcp/transports/header-policy.ts +1 -1
  98. package/src/modes/components/agent-hub-projection.ts +2 -2
  99. package/src/modes/components/agent-hub-renderer.ts +3 -7
  100. package/src/modes/controllers/command-controller.ts +4 -1
  101. package/src/modes/theme/color.ts +133 -0
  102. package/src/modes/theme/loader.ts +178 -0
  103. package/src/modes/theme/schema.ts +263 -0
  104. package/src/modes/theme/symbols.ts +1000 -0
  105. package/src/modes/theme/theme-class.ts +611 -0
  106. package/src/modes/theme/theme.ts +27 -2453
  107. package/src/modes/theme/tui-adapters.ts +279 -0
  108. package/src/registry/agent-registry.ts +2 -2
  109. package/src/secrets/index.ts +6 -12
  110. package/src/secrets/message-transform.ts +287 -0
  111. package/src/secrets/obfuscator.ts +39 -1303
  112. package/src/secrets/placeholder-scan.ts +506 -0
  113. package/src/secrets/placeholder.ts +309 -0
  114. package/src/secrets/replacement.ts +216 -0
  115. package/src/session/agent-session-types.ts +6 -0
  116. package/src/session/agent-session.ts +113 -7
  117. package/src/session/indexed-session-storage.ts +7 -2
  118. package/src/session/session-advisors.ts +32 -34
  119. package/src/session/session-handoff.ts +39 -20
  120. package/src/session/session-manager.ts +67 -3
  121. package/src/session/session-provider-boundary.ts +3 -6
  122. package/src/session/turn-recovery.ts +21 -12
  123. package/src/slash-commands/builtin-collaboration.ts +504 -0
  124. package/src/slash-commands/builtin-completions.ts +291 -0
  125. package/src/slash-commands/builtin-control.ts +78 -0
  126. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  127. package/src/slash-commands/builtin-marketplace.ts +567 -0
  128. package/src/slash-commands/builtin-modes.ts +518 -0
  129. package/src/slash-commands/builtin-registry.ts +21 -3000
  130. package/src/slash-commands/builtin-session.ts +592 -0
  131. package/src/task/executor.ts +17 -14
  132. package/src/tools/gh-common.ts +288 -0
  133. package/src/tools/gh-pr-checkout.ts +679 -0
  134. package/src/tools/gh-pr-diff.ts +473 -0
  135. package/src/tools/gh-run-watch.ts +1015 -0
  136. package/src/tools/gh-search.ts +500 -0
  137. package/src/tools/gh-types.ts +379 -0
  138. package/src/tools/gh-view.ts +612 -0
  139. package/src/tools/gh.ts +109 -3821
  140. package/src/tools/read-archive.ts +209 -0
  141. package/src/tools/read-format.ts +593 -0
  142. package/src/tools/read-path-resolution.ts +36 -0
  143. package/src/tools/read-pdf-images.ts +250 -0
  144. package/src/tools/read-renderer.ts +289 -0
  145. package/src/tools/read-selector.ts +84 -0
  146. package/src/tools/read-sqlite.ts +215 -0
  147. package/src/tools/read-summary.ts +199 -0
  148. package/src/tools/read.ts +77 -1820
  149. package/src/tools/shell-tokenize.ts +1 -1
  150. package/src/utils/changelog.ts +1 -1
  151. package/src/utils/title-generator.ts +3 -1
  152. package/src/web/search/providers/zai.ts +12 -3
@@ -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
  }