@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
@@ -1,22 +1,26 @@
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,
11
9
  executeWithKernelBase,
12
10
  getExecutionDeadlineMs,
13
- getRemainingTimeoutMs,
14
11
  isCancellationError,
15
12
  isTimedOutCancellation,
16
- resolveOwnerScopedSessionKey,
17
13
  waitForPromiseWithCancellation,
18
14
  } from "../executor-base";
19
15
  import type { JsStatusEvent } from "../js/shared/types";
16
+ import {
17
+ createKernelSessionRegistry,
18
+ formatSessionKernelTimeoutAnnotation,
19
+ formatSessionTimeoutAnnotation,
20
+ type KernelSession,
21
+ normalizeKernelSessionCwd,
22
+ requireRemainingKernelTimeoutMs,
23
+ } from "../kernel-session-registry";
20
24
  import { ensurePyToolBridge } from "../py/tool-bridge";
21
25
  import {
22
26
  checkRubyKernelAvailability,
@@ -97,39 +101,6 @@ export interface RubyResult {
97
101
  stdinRequested: boolean;
98
102
  }
99
103
 
100
- // ---------------------------------------------------------------------------
101
- // Session bookkeeping
102
- //
103
- // One RubyKernel subprocess per (session id, cwd, interpreter) tuple. The
104
- // runner mutates process-global cwd/$LOAD_PATH/ENV during execution, so
105
- // cross-directory work must never share a live kernel. Multiple agent owners can
106
- // register against the same tuple; the kernel stays alive until the last owner detaches.
107
- // ---------------------------------------------------------------------------
108
-
109
- interface RubySessionOwners {
110
- ownerIds: Set<string>;
111
- hasFallbackOwner: boolean;
112
- }
113
-
114
- interface RubySession extends RubySessionOwners {
115
- sessionKey: string;
116
- sessionId: string;
117
- cwd: string;
118
- kernel: RubyKernel;
119
- }
120
-
121
- interface StartingRubySession extends RubySessionOwners {
122
- promise: Promise<RubySession>;
123
- }
124
-
125
- const sessions = new Map<string, RubySession>();
126
- const startingSessions = new Map<string, StartingRubySession>();
127
- const resettingSessions = new Map<string, Promise<void>>();
128
-
129
- function normalizeSessionCwd(cwd: string): string {
130
- return path.resolve(cwd);
131
- }
132
-
133
104
  function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefined): string {
134
105
  if (interpreter === undefined) return "";
135
106
  const resolved = resolveExplicitRubyRuntime(interpreter, cwd, {}).rubyPath;
@@ -140,11 +111,6 @@ function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefin
140
111
  }
141
112
  }
142
113
 
143
- function buildSessionKey(sessionId: string, cwd: string, interpreter: string | undefined): string {
144
- const normalizedCwd = normalizeSessionCwd(cwd);
145
- return `${sessionId}\0${normalizedCwd}\0${normalizeExplicitInterpreter(normalizedCwd, interpreter)}`;
146
- }
147
-
148
114
  // ---------------------------------------------------------------------------
149
115
  // Cancellation plumbing
150
116
  // ---------------------------------------------------------------------------
@@ -160,32 +126,16 @@ class RubyExecutionCancelledError extends Error {
160
126
  }
161
127
 
162
128
  function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
163
- const remainingMs = getRemainingTimeoutMs(deadlineMs);
164
- if (remainingMs === undefined) return undefined;
165
- if (remainingMs <= 0) {
166
- throw new RubyExecutionCancelledError(true);
167
- }
168
- return remainingMs;
129
+ return requireRemainingKernelTimeoutMs(deadlineMs, RubyExecutionCancelledError);
169
130
  }
170
131
 
171
132
  // ---------------------------------------------------------------------------
172
133
  // Result formatting
173
134
  // ---------------------------------------------------------------------------
174
135
 
175
- function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
176
- if (timeoutMs === undefined) return "Command timed out";
177
- const secs = Math.max(1, Math.round(timeoutMs / 1000));
178
- return `Command timed out after ${secs} seconds`;
179
- }
136
+ const formatTimeoutAnnotation = formatSessionTimeoutAnnotation;
180
137
 
181
- function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
182
- const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
183
- if (kernelKilled) {
184
- 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.";
185
- }
186
- const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
187
- return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
188
- }
138
+ const formatKernelTimeoutAnnotation = formatSessionKernelTimeoutAnnotation;
189
139
 
190
140
  function createCancelledRubyResult(timedOut: boolean, timeoutMs?: number): RubyResult {
191
141
  const output = timedOut ? (formatTimeoutAnnotation(timeoutMs) ?? "Command timed out") : "";
@@ -207,161 +157,6 @@ async function startKernel(cwd: string, options: RubyExecutorOptions): Promise<R
207
157
  });
208
158
  }
209
159
 
210
- async function acquireSession(
211
- sessionKey: string,
212
- sessionId: string,
213
- cwd: string,
214
- options: RubyExecutorOptions,
215
- ): Promise<RubySession> {
216
- const existing = sessions.get(sessionKey);
217
- if (existing) {
218
- attachSessionOwner(existing, sessionId, options.kernelOwnerId);
219
- return existing;
220
- }
221
- const starting = startingSessions.get(sessionKey);
222
- if (starting) {
223
- attachSessionOwner(starting, sessionId, options.kernelOwnerId);
224
- return await starting.promise;
225
- }
226
- let startingSession!: StartingRubySession;
227
- const startup = (async () => {
228
- const kernel = await startKernel(cwd, options);
229
- const session: RubySession = {
230
- sessionKey,
231
- sessionId,
232
- cwd,
233
- kernel,
234
- ownerIds: new Set(startingSession.ownerIds),
235
- hasFallbackOwner: startingSession.hasFallbackOwner,
236
- };
237
- if (startingSessions.get(sessionKey) === startingSession) {
238
- sessions.set(sessionKey, session);
239
- }
240
- return session;
241
- })();
242
- startingSession = {
243
- ownerIds: new Set(),
244
- hasFallbackOwner: false,
245
- promise: startup,
246
- };
247
- attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
248
- startingSessions.set(sessionKey, startingSession);
249
- try {
250
- return await startup;
251
- } finally {
252
- if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
253
- }
254
- }
255
-
256
- async function replaceSessionKernel(session: RubySession, cwd: string, options: RubyExecutorOptions): Promise<void> {
257
- const old = session.kernel;
258
- const remaining = getRemainingTimeoutMs(options.deadlineMs);
259
- await old
260
- .shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
261
- .catch(() => undefined);
262
- if (sessions.get(session.sessionKey) !== session) {
263
- throw new RubyExecutionCancelledError(false);
264
- }
265
- requireRemainingTimeoutMs(options.deadlineMs);
266
- const next = await startKernel(cwd, options);
267
- if (sessions.get(session.sessionKey) !== session) {
268
- await next.shutdown().catch(() => undefined);
269
- throw new RubyExecutionCancelledError(false);
270
- }
271
- session.kernel = next;
272
- }
273
-
274
- async function resetSession(sessionKey: string): Promise<void> {
275
- const existing =
276
- sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
277
- if (!existing) return;
278
- sessions.delete(sessionKey);
279
- await existing.kernel.shutdown().catch(() => undefined);
280
- }
281
-
282
- // ---------------------------------------------------------------------------
283
- // Public dispose entry points
284
- // ---------------------------------------------------------------------------
285
-
286
- export async function disposeAllRubyKernelSessions(): Promise<void> {
287
- const pending = [...startingSessions.values()].map(starting => starting.promise);
288
- startingSessions.clear();
289
- const started = await Promise.allSettled(pending);
290
- const all = [...sessions.entries()];
291
- for (const result of started) {
292
- if (result.status !== "fulfilled") continue;
293
- if (!all.some(([, session]) => session === result.value)) {
294
- all.push([result.value.sessionKey, result.value]);
295
- }
296
- }
297
- for (const [id, session] of all) {
298
- if (sessions.get(id) === session) sessions.delete(id);
299
- }
300
- const results = await Promise.allSettled(all.map(([, session]) => session.kernel.shutdown()));
301
- for (let i = 0; i < all.length; i += 1) {
302
- const [id, session] = all[i];
303
- const result = results[i];
304
- if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
305
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
306
- logger.warn("Ruby kernel shutdown not confirmed", {
307
- sessionId: session.sessionId,
308
- sessionKey: id,
309
- cwd: session.cwd,
310
- reason,
311
- });
312
- if (!sessions.has(id)) sessions.set(id, session);
313
- }
314
- }
315
-
316
- export async function disposeRubyKernelSessionsByOwner(ownerId: string): Promise<void> {
317
- const toShutdown: RubySession[] = [];
318
- const startingToShutdown: StartingRubySession[] = [];
319
- for (const session of [...sessions.values()]) {
320
- if (!session.ownerIds.has(ownerId)) continue;
321
- if (session.ownerIds.size === 1) {
322
- toShutdown.push(session);
323
- continue;
324
- }
325
- session.ownerIds.delete(ownerId);
326
- }
327
- for (const [sessionKey, starting] of [...startingSessions.entries()]) {
328
- if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
329
- if (starting.ownerIds.size === 1) {
330
- startingSessions.delete(sessionKey);
331
- startingToShutdown.push(starting);
332
- continue;
333
- }
334
- starting.ownerIds.delete(ownerId);
335
- }
336
- for (const session of toShutdown) {
337
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
338
- }
339
- const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
340
- for (const result of started) {
341
- if (result.status !== "fulfilled") continue;
342
- const session = result.value;
343
- if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
344
- toShutdown.push(session);
345
- }
346
- const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
347
- for (let i = 0; i < toShutdown.length; i += 1) {
348
- const session = toShutdown[i];
349
- const result = results[i];
350
- if (result.status === "fulfilled" && result.value?.confirmed !== false) {
351
- session.ownerIds.delete(ownerId);
352
- continue;
353
- }
354
- const reason = result.status === "rejected" ? result.reason : "not confirmed";
355
- logger.warn("Ruby kernel shutdown not confirmed", {
356
- sessionId: session.sessionId,
357
- sessionKey: session.sessionKey,
358
- cwd: session.cwd,
359
- reason,
360
- });
361
- if (!sessions.has(session.sessionKey)) sessions.set(session.sessionKey, session);
362
- }
363
- }
364
-
365
160
  // ---------------------------------------------------------------------------
366
161
  // Execution
367
162
  // ---------------------------------------------------------------------------
@@ -406,67 +201,29 @@ async function ensureToolBridge(options: RubyExecutorOptions): Promise<void> {
406
201
  }
407
202
  }
408
203
 
409
- async function executeOnSession(code: string, cwd: string, options: RubyExecutorOptions): Promise<RubyResult> {
410
- const sessionId = options.sessionId ?? `session:${cwd}`;
411
- const sessionKey = resolveOwnerScopedSessionKey({
412
- baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
413
- ownerId: options.kernelOwnerId,
414
- reset: options.reset === true,
415
- hasSession: key => sessions.has(key) || startingSessions.has(key),
416
- getOwners: key => sessions.get(key) ?? startingSessions.get(key),
417
- });
418
- if (options.bridge && !options.bridgeSessionId) {
419
- options.bridgeSessionId = sessionId;
420
- }
421
- if (options.reset) {
422
- const inFlight = resettingSessions.get(sessionKey);
423
- if (inFlight) await inFlight.catch(() => undefined);
424
- else {
425
- const resetPromise = resetSession(sessionKey);
426
- resettingSessions.set(
427
- sessionKey,
428
- resetPromise.then(() => undefined),
429
- );
430
- try {
431
- await resetPromise;
432
- } finally {
433
- resettingSessions.delete(sessionKey);
434
- }
435
- }
436
- } else {
437
- const inFlight = resettingSessions.get(sessionKey);
438
- if (inFlight) await inFlight.catch(() => undefined);
439
- }
440
- const session = await acquireSession(sessionKey, sessionId, cwd, options);
441
- if (options.signal?.aborted) {
442
- throw new RubyExecutionCancelledError(
443
- isTimedOutCancellation(options.signal.reason, RubyExecutionCancelledError, options.signal),
444
- );
445
- }
446
- if (sessions.get(session.sessionKey) !== session) {
447
- throw new RubyExecutionCancelledError(false);
448
- }
449
- if (!session.kernel.isAlive()) {
450
- await replaceSessionKernel(session, cwd, options);
451
- if (sessions.get(session.sessionKey) !== session) {
452
- throw new RubyExecutionCancelledError(false);
453
- }
454
- }
455
- const runOptions = { ...options, cwd };
456
- try {
457
- return await executeWithKernel(session.kernel, code, runOptions);
458
- } catch (err) {
459
- if (isCancellationError(err, RubyExecutionCancelledError) || options.signal?.aborted) throw err;
460
- if (session.kernel.isAlive()) throw err;
461
- if (sessions.get(session.sessionKey) !== session) {
462
- throw new RubyExecutionCancelledError(false);
463
- }
464
- await replaceSessionKernel(session, cwd, options);
465
- if (sessions.get(session.sessionKey) !== session) {
466
- throw new RubyExecutionCancelledError(false);
467
- }
468
- return await executeWithKernel(session.kernel, code, runOptions);
469
- }
204
+ const sessionRegistry = createKernelSessionRegistry<
205
+ RubyKernel,
206
+ RubyExecutorOptions,
207
+ RubyResult,
208
+ KernelSession<RubyKernel>
209
+ >({
210
+ languageLabel: "Ruby",
211
+ cancelledErrorClass: RubyExecutionCancelledError,
212
+ buildSessionKey: (sessionId, cwd, interpreter) => {
213
+ const normalizedCwd = normalizeKernelSessionCwd(cwd);
214
+ return `${sessionId}\0${normalizedCwd}\0${normalizeExplicitInterpreter(normalizedCwd, interpreter)}`;
215
+ },
216
+ createSession: session => session,
217
+ startKernel,
218
+ executeWithKernel,
219
+ });
220
+
221
+ export async function disposeAllRubyKernelSessions(): Promise<void> {
222
+ await sessionRegistry.disposeAll();
223
+ }
224
+
225
+ export async function disposeRubyKernelSessionsByOwner(ownerId: string): Promise<void> {
226
+ await sessionRegistry.disposeByOwner(ownerId);
470
227
  }
471
228
 
472
229
  export async function executeRubyWithKernel(
@@ -478,7 +235,7 @@ export async function executeRubyWithKernel(
478
235
  }
479
236
 
480
237
  export async function executeRuby(code: string, options?: RubyExecutorOptions): Promise<RubyResult> {
481
- const cwd = normalizeSessionCwd(options?.cwd ?? getProjectDir());
238
+ const cwd = normalizeKernelSessionCwd(options?.cwd ?? getProjectDir());
482
239
  const deadlineMs = getExecutionDeadlineMs(options);
483
240
  const executionOptions: RubyExecutorOptions = {
484
241
  ...(options ?? {}),
@@ -499,7 +256,7 @@ export async function executeRuby(code: string, options?: RubyExecutorOptions):
499
256
  }
500
257
  await ensureKernelAvailable(cwd, executionOptions);
501
258
  await ensureToolBridge(executionOptions);
502
- return await executeOnSession(code, cwd, executionOptions);
259
+ return await sessionRegistry.executeOnSession(code, cwd, executionOptions);
503
260
  } catch (err) {
504
261
  if (isCancellationError(err, RubyExecutionCancelledError) || executionOptions.signal?.aborted) {
505
262
  return createCancelledRubyResult(
@@ -24,7 +24,8 @@ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-a
24
24
  import { $which, logger } from "@oh-my-pi/pi-utils";
25
25
  import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
26
26
  import { $ } from "bun";
27
- import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
27
+ import { obfuscateToolArguments } from "../secrets/message-transform";
28
+ import type { SecretObfuscator } from "../secrets/obfuscator";
28
29
  import { type SessionEntry, type SessionHeader, TITLE_CHANGE_ENTRY_TYPE } from "../session/session-entries";
29
30
  import type { SessionManager } from "../session/session-manager";
30
31
  import type { OutputMeta } from "../tools/output-meta";