@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
@@ -1,10 +1,8 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
 
4
3
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
5
4
  import type { ToolSession } from "../../tools";
6
5
  import {
7
- attachSessionOwner,
8
6
  buildManagedKernelEnv,
9
7
  buildManagedKernelEnvPatch,
10
8
  createCancelledKernelResult,
@@ -13,11 +11,18 @@ import {
13
11
  getRemainingTimeoutMs,
14
12
  isCancellationError,
15
13
  isTimedOutCancellation,
16
- resolveOwnerScopedSessionKey,
17
- type SessionOwners,
18
14
  waitForPromiseWithCancellation,
19
15
  } from "../executor-base";
20
16
  import type { JsStatusEvent } from "../js/shared/types";
17
+ import {
18
+ createKernelSessionRegistry,
19
+ formatSessionKernelTimeoutAnnotation,
20
+ formatSessionTimeoutAnnotation,
21
+ type KernelSession,
22
+ type KernelSessionRegistryContext,
23
+ normalizeKernelSessionCwd,
24
+ requireRemainingKernelTimeoutMs,
25
+ } from "../kernel-session-registry";
21
26
  import {
22
27
  checkPythonKernelAvailability,
23
28
  type KernelDisplayOutput,
@@ -145,27 +150,9 @@ interface SessionKernelReplacement {
145
150
  promise: Promise<PythonKernel>;
146
151
  }
147
152
 
148
- interface PythonSession {
149
- sessionKey: string;
150
- sessionId: string;
151
- cwd: string;
152
- kernel: PythonKernel;
153
+ interface PythonSession extends KernelSession<PythonKernel> {
153
154
  generation: number;
154
155
  replacement?: SessionKernelReplacement;
155
- ownerIds: Set<string>;
156
- hasFallbackOwner: boolean;
157
- }
158
-
159
- interface StartingPythonSession extends SessionOwners {
160
- promise: Promise<PythonSession>;
161
- }
162
-
163
- const sessions = new Map<string, PythonSession>();
164
- const startingSessions = new Map<string, StartingPythonSession>();
165
- const resettingSessions = new Map<string, Promise<void>>();
166
-
167
- function normalizeSessionCwd(cwd: string): string {
168
- return path.resolve(cwd);
169
156
  }
170
157
 
171
158
  function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefined): string {
@@ -178,11 +165,6 @@ function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefin
178
165
  }
179
166
  }
180
167
 
181
- function buildSessionKey(sessionId: string, cwd: string, interpreter: string | undefined): string {
182
- const normalizedCwd = normalizeSessionCwd(cwd);
183
- return `${sessionId}\0${normalizedCwd}\0${normalizeExplicitInterpreter(normalizedCwd, interpreter)}`;
184
- }
185
-
186
168
  // ---------------------------------------------------------------------------
187
169
  // Cancellation plumbing
188
170
  // ---------------------------------------------------------------------------
@@ -198,32 +180,16 @@ class PythonExecutionCancelledError extends Error {
198
180
  }
199
181
 
200
182
  function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
201
- const remainingMs = getRemainingTimeoutMs(deadlineMs);
202
- if (remainingMs === undefined) return undefined;
203
- if (remainingMs <= 0) {
204
- throw new PythonExecutionCancelledError(true);
205
- }
206
- return remainingMs;
183
+ return requireRemainingKernelTimeoutMs(deadlineMs, PythonExecutionCancelledError);
207
184
  }
208
185
 
209
186
  // ---------------------------------------------------------------------------
210
187
  // Result formatting
211
188
  // ---------------------------------------------------------------------------
212
189
 
213
- function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
214
- if (timeoutMs === undefined) return "Command timed out";
215
- const secs = Math.max(1, Math.round(timeoutMs / 1000));
216
- return `Command timed out after ${secs} seconds`;
217
- }
190
+ const formatTimeoutAnnotation = formatSessionTimeoutAnnotation;
218
191
 
219
- function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
220
- const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
221
- if (kernelKilled) {
222
- return "eval cell timed out and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.";
223
- }
224
- const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
225
- return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
226
- }
192
+ const formatKernelTimeoutAnnotation = formatSessionKernelTimeoutAnnotation;
227
193
 
228
194
  function createCancelledPythonResult(timedOut: boolean, timeoutMs?: number): PythonResult {
229
195
  const output = timedOut ? (formatTimeoutAnnotation(timeoutMs) ?? "Command timed out") : "";
@@ -245,63 +211,14 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
245
211
  });
246
212
  }
247
213
 
248
- async function acquireSession(
249
- sessionKey: string,
250
- sessionId: string,
251
- cwd: string,
252
- options: PythonExecutorOptions,
253
- ): Promise<PythonSession> {
254
- const existing = sessions.get(sessionKey);
255
- if (existing) {
256
- attachSessionOwner(existing, sessionId, options.kernelOwnerId);
257
- return existing;
258
- }
259
- const starting = startingSessions.get(sessionKey);
260
- if (starting) {
261
- attachSessionOwner(starting, sessionId, options.kernelOwnerId);
262
- return await starting.promise;
263
- }
264
- let startingSession!: StartingPythonSession;
265
- const startup = (async () => {
266
- const kernel = await startKernel(cwd, options);
267
- const session: PythonSession = {
268
- sessionKey,
269
- sessionId,
270
- cwd,
271
- kernel,
272
- generation: 0,
273
- ownerIds: new Set(startingSession.ownerIds),
274
- hasFallbackOwner: startingSession.hasFallbackOwner,
275
- };
276
- // Publish only while this startup still owns the key: owner disposal or
277
- // a concurrent dispose-all may have already reaped the starting record,
278
- // and publishing here would resurrect a kernel that was just torn down.
279
- if (startingSessions.get(sessionKey) === startingSession) {
280
- sessions.set(sessionKey, session);
281
- }
282
- return session;
283
- })();
284
- startingSession = {
285
- ownerIds: new Set(),
286
- hasFallbackOwner: false,
287
- promise: startup,
288
- };
289
- attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
290
- startingSessions.set(sessionKey, startingSession);
291
- try {
292
- return await startup;
293
- } finally {
294
- if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
295
- }
296
- }
297
-
298
214
  async function replaceSessionKernel(
299
215
  session: PythonSession,
300
- kernel: PythonKernel,
301
- generation: number,
302
216
  cwd: string,
303
217
  options: PythonExecutorOptions,
218
+ context: KernelSessionRegistryContext<PythonKernel, PythonExecutorOptions, PythonSession>,
304
219
  ): Promise<PythonKernel> {
220
+ const kernel = session.kernel;
221
+ const generation = session.generation;
305
222
  const inFlight = session.replacement;
306
223
  if (inFlight?.generation === generation) {
307
224
  if (
@@ -312,7 +229,11 @@ async function replaceSessionKernel(
312
229
  }
313
230
  return await waitForPromiseWithCancellation(inFlight.promise, options, PythonExecutionCancelledError);
314
231
  }
315
- if (sessions.get(session.sessionKey) !== session || session.generation !== generation || session.kernel !== kernel) {
232
+ if (
233
+ context.sessions.get(session.sessionKey) !== session ||
234
+ session.generation !== generation ||
235
+ session.kernel !== kernel
236
+ ) {
316
237
  throw new PythonExecutionCancelledError(false);
317
238
  }
318
239
 
@@ -333,7 +254,7 @@ async function replaceSessionKernel(
333
254
  throw new PythonExecutionCancelledError(true);
334
255
  }
335
256
  if (
336
- sessions.get(session.sessionKey) !== session ||
257
+ context.sessions.get(session.sessionKey) !== session ||
337
258
  session.generation !== generation ||
338
259
  session.kernel !== kernel
339
260
  ) {
@@ -345,7 +266,7 @@ async function replaceSessionKernel(
345
266
  deadlineMs: undefined,
346
267
  });
347
268
  if (
348
- sessions.get(session.sessionKey) !== session ||
269
+ context.sessions.get(session.sessionKey) !== session ||
349
270
  session.generation !== generation ||
350
271
  session.kernel !== kernel
351
272
  ) {
@@ -374,111 +295,16 @@ async function acquireLiveSessionKernel(
374
295
  session: PythonSession,
375
296
  cwd: string,
376
297
  options: PythonExecutorOptions,
298
+ context: KernelSessionRegistryContext<PythonKernel, PythonExecutorOptions, PythonSession>,
377
299
  ): Promise<PythonKernel> {
378
- while (sessions.get(session.sessionKey) === session) {
300
+ while (context.sessions.get(session.sessionKey) === session) {
379
301
  const kernel = session.kernel;
380
- const generation = session.generation;
381
302
  if (kernel.isAlive()) return kernel;
382
- await replaceSessionKernel(session, kernel, generation, cwd, options);
303
+ await context.replaceSessionKernel(session, cwd, options);
383
304
  }
384
305
  throw new PythonExecutionCancelledError(false);
385
306
  }
386
307
 
387
- async function resetSession(sessionKey: string): Promise<void> {
388
- const existing =
389
- sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
390
- if (!existing) return;
391
- existing.generation += 1;
392
- sessions.delete(sessionKey);
393
- await shutdownInvalidatedSession(existing).catch(() => undefined);
394
- }
395
-
396
- // ---------------------------------------------------------------------------
397
- // Public dispose entry points
398
- // ---------------------------------------------------------------------------
399
-
400
- export async function disposeAllKernelSessions(): Promise<void> {
401
- const pending = [...startingSessions.values()].map(starting => starting.promise);
402
- startingSessions.clear();
403
- const started = await Promise.allSettled(pending);
404
- const all = [...sessions.entries()];
405
- for (const result of started) {
406
- if (result.status !== "fulfilled") continue;
407
- if (!all.some(([, session]) => session === result.value)) {
408
- all.push([result.value.sessionKey, result.value]);
409
- }
410
- }
411
- for (const [id, session] of all) {
412
- session.generation += 1;
413
- if (sessions.get(id) === session) sessions.delete(id);
414
- }
415
- const results = await Promise.allSettled(all.map(([, session]) => shutdownInvalidatedSession(session)));
416
- for (let i = 0; i < all.length; i += 1) {
417
- const [id, session] = all[i];
418
- const result = results[i];
419
- if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
420
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
421
- logger.warn("Python kernel shutdown not confirmed", {
422
- sessionId: session.sessionId,
423
- sessionKey: id,
424
- cwd: session.cwd,
425
- reason,
426
- });
427
- if (!sessions.has(id)) sessions.set(id, session);
428
- }
429
- }
430
-
431
- export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
432
- const toShutdown: PythonSession[] = [];
433
- for (const session of [...sessions.values()]) {
434
- if (!session.ownerIds.has(ownerId)) continue;
435
- if (session.ownerIds.size === 1) {
436
- toShutdown.push(session);
437
- continue;
438
- }
439
- session.ownerIds.delete(ownerId);
440
- }
441
- const startingToShutdown: StartingPythonSession[] = [];
442
- for (const [sessionKey, starting] of [...startingSessions.entries()]) {
443
- if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
444
- if (starting.ownerIds.size === 1) {
445
- startingSessions.delete(sessionKey);
446
- startingToShutdown.push(starting);
447
- continue;
448
- }
449
- starting.ownerIds.delete(ownerId);
450
- }
451
- for (const session of toShutdown) {
452
- session.generation += 1;
453
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
454
- }
455
- const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
456
- for (const result of started) {
457
- if (result.status !== "fulfilled") continue;
458
- const session = result.value;
459
- session.generation += 1;
460
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
461
- toShutdown.push(session);
462
- }
463
- const results = await Promise.allSettled(toShutdown.map(session => shutdownInvalidatedSession(session)));
464
- for (let i = 0; i < toShutdown.length; i += 1) {
465
- const session = toShutdown[i];
466
- const result = results[i];
467
- if (result.status === "fulfilled" && result.value?.confirmed !== false) {
468
- session.ownerIds.delete(ownerId);
469
- continue;
470
- }
471
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
472
- logger.warn("Python kernel shutdown not confirmed", {
473
- sessionId: session.sessionId,
474
- sessionKey: session.sessionKey,
475
- cwd: session.cwd,
476
- reason,
477
- });
478
- if (!sessions.has(session.sessionKey)) sessions.set(session.sessionKey, session);
479
- }
480
- }
481
-
482
308
  // ---------------------------------------------------------------------------
483
309
  // Execution
484
310
  // ---------------------------------------------------------------------------
@@ -535,65 +361,31 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
535
361
  }
536
362
  }
537
363
 
538
- async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
539
- const sessionId = options.sessionId ?? `session:${cwd}`;
540
- const sessionKey = resolveOwnerScopedSessionKey({
541
- baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
542
- ownerId: options.kernelOwnerId,
543
- reset: options.reset === true,
544
- hasSession: key => sessions.has(key) || startingSessions.has(key),
545
- getOwners: key => sessions.get(key) ?? startingSessions.get(key),
546
- });
547
- if (options.bridge && !options.bridgeSessionId) {
548
- options.bridgeSessionId = sessionId;
549
- }
550
- if (options.reset) {
551
- // Coalesce concurrent resets: if another reset is in flight for this
552
- // session, await it instead of throwing — the caller's intent ("start
553
- // from a clean kernel") is satisfied once that reset settles.
554
- const inFlight = resettingSessions.get(sessionKey);
555
- if (inFlight) await inFlight.catch(() => undefined);
556
- else {
557
- const resetPromise = resetSession(sessionKey);
558
- resettingSessions.set(
559
- sessionKey,
560
- resetPromise.then(() => undefined),
561
- );
562
- try {
563
- await resetPromise;
564
- } finally {
565
- resettingSessions.delete(sessionKey);
566
- }
567
- }
568
- } else {
569
- // A reset already in progress is an internal coordination state, not a
570
- // user-visible failure. Wait for it to clear, then proceed with the
571
- // requested execution on the freshly-restarted kernel.
572
- const inFlight = resettingSessions.get(sessionKey);
573
- if (inFlight) await inFlight.catch(() => undefined);
574
- }
575
- const session = await acquireSession(sessionKey, sessionId, cwd, options);
576
- if (options.signal?.aborted) {
577
- throw new PythonExecutionCancelledError(
578
- isTimedOutCancellation(options.signal.reason, PythonExecutionCancelledError, options.signal),
579
- );
580
- }
581
- const kernel = await acquireLiveSessionKernel(session, cwd, options);
582
- if (sessions.get(session.sessionKey) !== session || session.kernel !== kernel) {
583
- throw new PythonExecutionCancelledError(false);
584
- }
585
- const runOptions = { ...options, cwd };
586
- try {
587
- return await executeWithKernel(kernel, code, runOptions);
588
- } catch (err) {
589
- if (isCancellationError(err, PythonExecutionCancelledError) || options.signal?.aborted) throw err;
590
- if (kernel.isAlive()) throw err;
591
- const retryKernel = await acquireLiveSessionKernel(session, cwd, options);
592
- if (sessions.get(session.sessionKey) !== session || session.kernel !== retryKernel) {
593
- throw new PythonExecutionCancelledError(false);
594
- }
595
- return await executeWithKernel(retryKernel, code, runOptions);
596
- }
364
+ const sessionRegistry = createKernelSessionRegistry<PythonKernel, PythonExecutorOptions, PythonResult, PythonSession>({
365
+ languageLabel: "Python",
366
+ cancelledErrorClass: PythonExecutionCancelledError,
367
+ buildSessionKey: (sessionId, cwd, interpreter) => {
368
+ const normalizedCwd = normalizeKernelSessionCwd(cwd);
369
+ return `${sessionId}\0${normalizedCwd}\0${normalizeExplicitInterpreter(normalizedCwd, interpreter)}`;
370
+ },
371
+ createSession: session => ({ ...session, generation: 0 }),
372
+ startKernel,
373
+ executeWithKernel,
374
+ replaceSessionKernel,
375
+ acquireLiveSessionKernel,
376
+ invalidateSession: session => {
377
+ session.generation += 1;
378
+ },
379
+ shutdownSession: session => shutdownInvalidatedSession(session),
380
+ validateKernel: (session, kernel) => session.kernel === kernel,
381
+ });
382
+
383
+ export async function disposeAllKernelSessions(): Promise<void> {
384
+ await sessionRegistry.disposeAll();
385
+ }
386
+
387
+ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
388
+ await sessionRegistry.disposeByOwner(ownerId);
597
389
  }
598
390
 
599
391
  export async function executePythonWithKernel(
@@ -605,7 +397,7 @@ export async function executePythonWithKernel(
605
397
  }
606
398
 
607
399
  export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
608
- const cwd = normalizeSessionCwd(options?.cwd ?? getProjectDir());
400
+ const cwd = normalizeKernelSessionCwd(options?.cwd ?? getProjectDir());
609
401
  const deadlineMs = getExecutionDeadlineMs(options);
610
402
  const executionOptions: PythonExecutorOptions = {
611
403
  ...(options ?? {}),
@@ -631,7 +423,7 @@ export async function executePython(code: string, options?: PythonExecutorOption
631
423
  if (kernelMode === "per-call") {
632
424
  return await executePerCall(code, cwd, executionOptions);
633
425
  }
634
- return await executeOnSession(code, cwd, executionOptions);
426
+ return await sessionRegistry.executeOnSession(code, cwd, executionOptions);
635
427
  } catch (err) {
636
428
  if (isCancellationError(err, PythonExecutionCancelledError) || executionOptions.signal?.aborted) {
637
429
  return createCancelledPythonResult(