@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.15

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 (153) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli.js +5615 -2833
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +64 -14
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/export/share.d.ts +8 -1
  24. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  26. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  27. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  28. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  29. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  30. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  31. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  32. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  33. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  34. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +0 -2
  36. package/dist/types/modes/theme/theme.d.ts +8 -1
  37. package/dist/types/modes/types.d.ts +0 -2
  38. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  39. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  40. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/secrets/obfuscator.d.ts +3 -3
  43. package/dist/types/session/agent-session.d.ts +2 -2
  44. package/dist/types/stt/asr-client.d.ts +3 -29
  45. package/dist/types/subprocess/worker-client.d.ts +149 -0
  46. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  47. package/dist/types/tiny/title-client.d.ts +14 -34
  48. package/dist/types/tools/eval-backends.d.ts +6 -3
  49. package/dist/types/tools/eval-render.d.ts +6 -5
  50. package/dist/types/tools/eval.d.ts +13 -15
  51. package/dist/types/tools/index.d.ts +3 -3
  52. package/dist/types/tts/tts-client.d.ts +3 -28
  53. package/dist/types/tui/code-cell.d.ts +7 -0
  54. package/dist/types/utils/file-display-mode.d.ts +1 -1
  55. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  56. package/dist/types/web/parallel.d.ts +6 -0
  57. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  58. package/package.json +12 -12
  59. package/src/advisor/__tests__/advisor.test.ts +114 -0
  60. package/src/advisor/runtime.ts +129 -1
  61. package/src/config/model-registry.ts +12 -4
  62. package/src/config/settings-schema.ts +74 -18
  63. package/src/config/settings.ts +5 -0
  64. package/src/dap/client.ts +13 -107
  65. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  66. package/src/eval/backend-helpers.ts +48 -0
  67. package/src/eval/executor-base.ts +425 -0
  68. package/src/eval/index.ts +2 -0
  69. package/src/eval/jl/executor.ts +540 -0
  70. package/src/eval/jl/index.ts +54 -0
  71. package/src/eval/jl/kernel.ts +235 -0
  72. package/src/eval/jl/prelude.jl +930 -0
  73. package/src/eval/jl/prelude.ts +3 -0
  74. package/src/eval/jl/runner.jl +634 -0
  75. package/src/eval/jl/runtime.ts +118 -0
  76. package/src/eval/js/index.ts +3 -14
  77. package/src/eval/kernel-base.ts +569 -0
  78. package/src/eval/py/executor.ts +43 -252
  79. package/src/eval/py/index.ts +9 -20
  80. package/src/eval/py/kernel.ts +29 -544
  81. package/src/eval/rb/executor.ts +504 -0
  82. package/src/eval/rb/index.ts +54 -0
  83. package/src/eval/rb/kernel.ts +230 -0
  84. package/src/eval/rb/prelude.rb +721 -0
  85. package/src/eval/rb/prelude.ts +3 -0
  86. package/src/eval/rb/runner.rb +474 -0
  87. package/src/eval/rb/runtime.ts +132 -0
  88. package/src/eval/runtime-env.ts +104 -0
  89. package/src/eval/types.ts +3 -3
  90. package/src/exec/bash-executor.ts +44 -0
  91. package/src/export/share.ts +51 -28
  92. package/src/extensibility/extensions/runner.ts +4 -11
  93. package/src/extensibility/hooks/loader.ts +3 -21
  94. package/src/extensibility/session-handler-types.ts +21 -0
  95. package/src/internal-urls/docs-index.generated.txt +1 -1
  96. package/src/jsonrpc/message-framing.ts +142 -0
  97. package/src/lsp/client.ts +13 -109
  98. package/src/mcp/transports/stdio.ts +20 -4
  99. package/src/mnemopi/embed-client.ts +43 -198
  100. package/src/modes/components/agent-dashboard.ts +17 -40
  101. package/src/modes/components/chat-transcript-builder.ts +18 -102
  102. package/src/modes/components/custom-editor.test.ts +22 -0
  103. package/src/modes/components/custom-editor.ts +29 -1
  104. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  105. package/src/modes/components/extensions/extension-list.ts +16 -44
  106. package/src/modes/components/footer.ts +4 -3
  107. package/src/modes/components/history-search.ts +4 -16
  108. package/src/modes/components/selector-helpers.ts +129 -0
  109. package/src/modes/components/settings-selector.ts +7 -5
  110. package/src/modes/components/status-line/component.ts +5 -1
  111. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  112. package/src/modes/components/status-line/segments.ts +1 -1
  113. package/src/modes/components/status-line/types.ts +1 -0
  114. package/src/modes/components/tree-selector.ts +13 -18
  115. package/src/modes/controllers/command-controller.ts +1 -0
  116. package/src/modes/controllers/event-controller.ts +3 -9
  117. package/src/modes/controllers/input-controller.ts +32 -54
  118. package/src/modes/interactive-mode.ts +5 -7
  119. package/src/modes/theme/theme.ts +35 -0
  120. package/src/modes/types.ts +0 -2
  121. package/src/modes/utils/copy-targets.ts +3 -2
  122. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  123. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  124. package/src/modes/utils/ui-helpers.ts +21 -126
  125. package/src/prompts/system/system-prompt.md +5 -5
  126. package/src/prompts/tools/bash.md +2 -3
  127. package/src/prompts/tools/eval.md +6 -4
  128. package/src/prompts/tools/find.md +0 -4
  129. package/src/prompts/tools/read.md +1 -2
  130. package/src/prompts/tools/replace.md +1 -1
  131. package/src/sdk.ts +13 -7
  132. package/src/secrets/obfuscator.ts +3 -9
  133. package/src/session/agent-session.ts +42 -9
  134. package/src/slash-commands/builtin-registry.ts +2 -1
  135. package/src/stt/asr-client.ts +35 -215
  136. package/src/stt/asr-worker.ts +29 -181
  137. package/src/subprocess/worker-client.ts +297 -0
  138. package/src/subprocess/worker-runtime.ts +277 -0
  139. package/src/task/executor.ts +4 -4
  140. package/src/tiny/title-client.ts +53 -219
  141. package/src/tiny/worker.ts +29 -180
  142. package/src/tools/eval-backends.ts +10 -3
  143. package/src/tools/eval-render.ts +17 -8
  144. package/src/tools/eval.ts +187 -22
  145. package/src/tools/index.ts +51 -28
  146. package/src/tts/tts-client.ts +38 -206
  147. package/src/tts/tts-worker.ts +23 -97
  148. package/src/tui/code-cell.ts +12 -1
  149. package/src/utils/file-display-mode.ts +2 -3
  150. package/src/utils/shell-snapshot.ts +63 -1
  151. package/src/web/parallel.ts +43 -42
  152. package/src/web/search/providers/parallel.ts +10 -99
  153. package/src/web/search/providers/perplexity.ts +18 -6
@@ -11,10 +11,9 @@ import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
14
- import type { Subprocess } from "bun";
15
14
  import { $ } from "bun";
16
15
  import { Settings } from "../../config/settings";
17
- import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
16
+ import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
18
17
  import { PYTHON_PRELUDE } from "./prelude";
19
18
  import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
20
19
  import {
@@ -26,6 +25,14 @@ import {
26
25
  } from "./runtime";
27
26
  import { hostHasInheritableConsole, shouldHideKernelWindow } from "./spawn-options";
28
27
 
28
+ export type {
29
+ KernelExecuteOptions,
30
+ KernelExecuteResult,
31
+ KernelRuntimeEnv,
32
+ KernelShutdownOptions,
33
+ KernelShutdownResult,
34
+ } from "../kernel-base";
35
+
29
36
  export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
30
37
  export { renderKernelDisplay } from "./display";
31
38
 
@@ -58,62 +65,6 @@ const STARTUP_TIMEOUT_MS = 10_000;
58
65
  // kernel's state, so we only kill as a last-resort recovery path.
59
66
  const INTERRUPT_ESCALATION_MS = 5_000;
60
67
 
61
- export type KernelRuntimeEnv = Record<string, string | null>;
62
-
63
- export interface KernelExecuteOptions {
64
- id?: string;
65
- /** Runtime working directory applied immediately before this request executes. */
66
- cwd?: string;
67
- /** Managed runtime environment variables applied immediately before this request executes. */
68
- env?: KernelRuntimeEnv;
69
- signal?: AbortSignal;
70
- onChunk?: (text: string) => Promise<void> | void;
71
- onDisplay?: (output: KernelDisplayOutput) => Promise<void> | void;
72
- timeoutMs?: number;
73
- silent?: boolean;
74
- storeHistory?: boolean;
75
- allowStdin?: boolean;
76
- }
77
-
78
- export interface KernelExecuteResult {
79
- status: "ok" | "error";
80
- executionCount?: number;
81
- error?: { name: string; value: string; traceback: string[] };
82
- cancelled: boolean;
83
- timedOut: boolean;
84
- stdinRequested: boolean;
85
- /**
86
- * True when the kernel subprocess was killed as part of settling this
87
- * execution (e.g. SIGINT was ignored and we escalated to shutdown, or the
88
- * kernel died unexpectedly). When false, the kernel remains reusable.
89
- */
90
- kernelKilled?: boolean;
91
- }
92
-
93
- export interface KernelShutdownResult {
94
- confirmed: boolean;
95
- }
96
-
97
- interface KernelLifecycleOptions {
98
- signal?: AbortSignal;
99
- deadlineMs?: number;
100
- }
101
-
102
- interface KernelStartOptions extends KernelLifecycleOptions {
103
- cwd: string;
104
- env?: Record<string, string | undefined>;
105
- /**
106
- * Explicit interpreter path (`python.interpreter` from the session's
107
- * settings). When set, runtime discovery is skipped entirely.
108
- */
109
- interpreter?: string;
110
- }
111
-
112
- interface KernelShutdownOptions {
113
- signal?: AbortSignal;
114
- timeoutMs?: number;
115
- }
116
-
117
68
  export interface PythonKernelAvailability {
118
69
  ok: boolean;
119
70
  pythonPath?: string;
@@ -122,24 +73,6 @@ export interface PythonKernelAvailability {
122
73
  runtime?: PythonRuntime;
123
74
  }
124
75
 
125
- function getRemainingTimeMs(deadlineMs?: number): number | undefined {
126
- if (deadlineMs === undefined) return undefined;
127
- return Math.max(0, deadlineMs - Date.now());
128
- }
129
-
130
- function createAbortError(name: "AbortError" | "TimeoutError", message: string): Error {
131
- const err = new Error(message);
132
- err.name = name;
133
- return err;
134
- }
135
-
136
- function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
137
- if (!signal?.aborted) return;
138
- const reason = signal.reason;
139
- if (reason instanceof Error) throw reason;
140
- throw createAbortError("AbortError", typeof reason === "string" ? reason : fallbackReason);
141
- }
142
-
143
76
  // Cache successful probes per resolved cwd + explicit interpreter: every cell
144
77
  // otherwise pays one (or two — backend.isAvailable + ensureKernelAvailable)
145
78
  // interpreter spawns even when the kernel is already hot. Failures are not
@@ -207,48 +140,24 @@ async function probePythonKernelAvailability(cwd: string, interpreter?: string):
207
140
  }
208
141
  }
209
142
 
210
- type FrameType = "started" | "stdout" | "stderr" | "display" | "result" | "error" | "done";
211
-
212
- interface Frame {
213
- type: FrameType;
214
- id?: string;
215
- data?: string;
216
- bundle?: Record<string, unknown>;
217
- ename?: string;
218
- evalue?: string;
219
- traceback?: string[];
220
- status?: "ok" | "error";
221
- executionCount?: number;
222
- cancelled?: boolean;
223
- }
224
-
225
- interface PendingExecution {
226
- resolve: (result: KernelExecuteResult) => void;
227
- options?: KernelExecuteOptions;
228
- status: "ok" | "error";
229
- executionCount?: number;
230
- error?: { name: string; value: string; traceback: string[] };
231
- cancelled: boolean;
232
- timedOut: boolean;
233
- stdinRequested: boolean;
234
- kernelKilled: boolean;
235
- settled: boolean;
236
- escalationTimer?: NodeJS.Timeout;
237
- }
238
-
239
- export class PythonKernel {
240
- readonly id: string;
241
- #proc: Subprocess | null = null;
242
- #stdin: Bun.FileSink | null = null;
243
- #alive = true;
244
- #disposed = false;
245
- #shutdownConfirmed = false;
246
- #exitedPromise: Promise<number> | null = null;
247
- #pending = new Map<string, PendingExecution>();
248
- #readBuffer = "";
249
-
143
+ export class PythonKernel extends BaseKernel {
250
144
  private constructor(id: string) {
251
- this.id = id;
145
+ super(id, {
146
+ languageName: "Python",
147
+ traceIpc: TRACE_IPC,
148
+ exitPayload: JSON.stringify({ type: "exit" }),
149
+ interruptEscalationMs: INTERRUPT_ESCALATION_MS,
150
+ shutdownGraceMs: SHUTDOWN_GRACE_MS,
151
+ buildPayload: (code, msgId, opts) =>
152
+ JSON.stringify({
153
+ id: msgId,
154
+ code,
155
+ cwd: opts?.cwd,
156
+ env: opts?.env,
157
+ silent: opts?.silent ?? false,
158
+ storeHistory: opts?.storeHistory ?? !(opts?.silent ?? false),
159
+ }),
160
+ });
252
161
  }
253
162
 
254
163
  static async start(options: KernelStartOptions): Promise<PythonKernel> {
@@ -262,10 +171,6 @@ export class PythonKernel {
262
171
  throw new Error(availability.reason ?? "Python kernel unavailable");
263
172
  }
264
173
 
265
- // Reuse the interpreter the availability probe selected so the spawned
266
- // kernel matches what we verified actually runs. The fallback computes a
267
- // runtime only for the skip-check fast path (test runtime /
268
- // PI_PYTHON_SKIP_CHECK), where no candidate was probed.
269
174
  let runtime = availability.runtime;
270
175
  if (!runtime) {
271
176
  const { env: shellEnv } = (await Settings.init()).getShellConfig();
@@ -280,7 +185,6 @@ export class PythonKernel {
280
185
  for (const [key, value] of Object.entries(options.env ?? {})) {
281
186
  if (typeof value === "string") spawnEnv[key] = value;
282
187
  }
283
- // Unbuffered IO is critical for streaming.
284
188
  spawnEnv.PYTHONUNBUFFERED = "1";
285
189
  spawnEnv.PYTHONIOENCODING = "utf-8";
286
190
 
@@ -293,447 +197,28 @@ export class PythonKernel {
293
197
  stdin: "pipe",
294
198
  stdout: "pipe",
295
199
  stderr: "pipe",
296
- // Detached from any inherited console only when the host itself
297
- // has no console — kernel32!GetConsoleWindow() is authoritative
298
- // (works even when every stdio stream is redirected), with a
299
- // TTY-OR fallback when the FFI probe is unavailable. See #1960
300
- // for the numpy/pandas LoadLibraryExW hang + SIGINT-recovery
301
- // failure that motivates the predicate.
302
200
  windowsHide: shouldHideKernelWindow({
303
201
  platform: process.platform,
304
202
  hostHasInheritableConsole: hostHasInheritableConsole(),
305
203
  }),
306
204
  });
307
- kernel.#proc = proc;
308
- kernel.#stdin = proc.stdin;
309
- kernel.#exitedPromise = proc.exited;
310
- void kernel.#exitedPromise.then(code => {
311
- kernel.#alive = false;
312
- kernel.#abortPendingExecutions(`Python kernel exited with code ${code}`, { kernelKilled: true });
313
- });
314
205
 
315
- kernel.#startReader(proc.stdout as ReadableStream<Uint8Array>);
316
- kernel.#startStderrDrain(proc.stderr as ReadableStream<Uint8Array>);
206
+ kernel.setProcess(proc);
317
207
 
318
208
  const startup = { signal: options.signal, deadlineMs: options.deadlineMs };
319
209
  const startupBudget = Math.min(getRemainingTimeMs(startup.deadlineMs) ?? STARTUP_TIMEOUT_MS, STARTUP_TIMEOUT_MS);
320
210
 
321
211
  try {
322
212
  const initScript = buildInitScript(options.cwd, options.env);
323
- await kernel.#executeWithBudget(initScript, startup.signal, startupBudget, "Python kernel init");
324
- await kernel.#executeWithBudget(PYTHON_PRELUDE, startup.signal, startupBudget, "Python kernel prelude");
213
+ await kernel.executeWithBudget(initScript, startup.signal, startupBudget, "Python kernel init");
214
+ await kernel.executeWithBudget(PYTHON_PRELUDE, startup.signal, startupBudget, "Python kernel prelude");
325
215
  return kernel;
326
216
  } catch (err) {
327
217
  await kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }).catch(() => {});
328
218
  throw err;
329
219
  }
330
220
  }
331
-
332
- isAlive(): boolean {
333
- return this.#alive && !this.#disposed;
334
- }
335
-
336
- async execute(code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
337
- if (!this.isAlive()) {
338
- throw new Error("Python kernel is not running");
339
- }
340
-
341
- const msgId = options?.id ?? Snowflake.next();
342
- const { promise, resolve } = Promise.withResolvers<KernelExecuteResult>();
343
- const pending: PendingExecution = {
344
- resolve,
345
- options,
346
- status: "ok",
347
- cancelled: false,
348
- timedOut: false,
349
- stdinRequested: false,
350
- settled: false,
351
- kernelKilled: false,
352
- };
353
- this.#pending.set(msgId, pending);
354
-
355
- const finalize = () => {
356
- if (pending.settled) return;
357
- pending.settled = true;
358
- this.#pending.delete(msgId);
359
- cleanup();
360
- resolve({
361
- status: pending.status,
362
- executionCount: pending.executionCount,
363
- error: pending.error,
364
- cancelled: pending.cancelled,
365
- timedOut: pending.timedOut,
366
- stdinRequested: pending.stdinRequested,
367
- kernelKilled: pending.kernelKilled,
368
- });
369
- };
370
-
371
- const requestCancel = () => {
372
- if (pending.settled || pending.escalationTimer) return;
373
- void this.interrupt();
374
- const escalation = setTimeout(() => {
375
- if (pending.settled) return;
376
- logger.warn("Python runner did not respond to SIGINT; terminating subprocess", {
377
- kernelId: this.id,
378
- });
379
- // SIGINT was ignored; mark the cell as kernel-killed so callers can
380
- // surface the harsher recovery message. `shutdown()` aborts pending
381
- // executions immediately and escalates to SIGTERM/SIGKILL, so the
382
- // host queue unblocks even if the runner is stuck in a
383
- // non-interruptible state.
384
- pending.kernelKilled = true;
385
- void this.shutdown();
386
- }, INTERRUPT_ESCALATION_MS);
387
- escalation.unref?.();
388
- pending.escalationTimer = escalation;
389
- };
390
-
391
- const onAbort = () => {
392
- pending.cancelled = true;
393
- pending.timedOut = pending.timedOut || isTimeoutReason(options?.signal?.reason);
394
- requestCancel();
395
- };
396
- const timeoutId =
397
- typeof options?.timeoutMs === "number" && options.timeoutMs > 0
398
- ? setTimeout(() => {
399
- pending.timedOut = true;
400
- pending.cancelled = true;
401
- requestCancel();
402
- }, options.timeoutMs)
403
- : undefined;
404
-
405
- const cleanup = () => {
406
- if (timeoutId) clearTimeout(timeoutId);
407
- if (pending.escalationTimer) clearTimeout(pending.escalationTimer);
408
- pending.escalationTimer = undefined;
409
- options?.signal?.removeEventListener("abort", onAbort);
410
- };
411
-
412
- if (options?.signal) {
413
- if (options.signal.aborted) {
414
- onAbort();
415
- } else {
416
- options.signal.addEventListener("abort", onAbort, { once: true });
417
- }
418
- }
419
-
420
- // Stash finalize on the pending entry so the reader can call it on `done`.
421
- (pending as PendingExecution & { finalize: () => void }).finalize = finalize;
422
-
423
- const payload = JSON.stringify({
424
- id: msgId,
425
- code,
426
- cwd: options?.cwd,
427
- env: options?.env,
428
- silent: options?.silent ?? false,
429
- storeHistory: options?.storeHistory ?? !(options?.silent ?? false),
430
- });
431
-
432
- try {
433
- await this.#writeLine(payload);
434
- } catch (err) {
435
- pending.cancelled = true;
436
- pending.error = {
437
- name: "TransportError",
438
- value: err instanceof Error ? err.message : String(err),
439
- traceback: [],
440
- };
441
- finalize();
442
- }
443
-
444
- return promise;
445
- }
446
-
447
- async interrupt(): Promise<void> {
448
- if (!this.#proc || this.#disposed) return;
449
- try {
450
- this.#proc.kill("SIGINT");
451
- } catch (err) {
452
- logger.warn("Failed to interrupt python runner", { error: err instanceof Error ? err.message : String(err) });
453
- }
454
- }
455
-
456
- async shutdown(options?: KernelShutdownOptions): Promise<KernelShutdownResult> {
457
- if (this.#shutdownConfirmed) return { confirmed: true };
458
-
459
- this.#alive = false;
460
- this.#abortPendingExecutions("Python kernel shutdown", { kernelKilled: true });
461
-
462
- const timeoutMs = options?.timeoutMs ?? SHUTDOWN_GRACE_MS;
463
- const proc = this.#proc;
464
- if (!proc) {
465
- this.#shutdownConfirmed = true;
466
- this.#disposed = true;
467
- return { confirmed: true };
468
- }
469
-
470
- try {
471
- await this.#writeLine(JSON.stringify({ type: "exit" })).catch(() => {});
472
- } catch {
473
- /* writer may already be closed */
474
- }
475
-
476
- try {
477
- this.#stdin?.end();
478
- } catch {
479
- /* ignore */
480
- }
481
-
482
- const exited = this.#waitForExitWithTimeout(timeoutMs);
483
- let result = await exited;
484
- if (!result) {
485
- try {
486
- proc.kill("SIGTERM");
487
- } catch {
488
- /* ignore */
489
- }
490
- result = await this.#waitForExitWithTimeout(timeoutMs);
491
- }
492
- if (!result) {
493
- try {
494
- proc.kill("SIGKILL");
495
- } catch {
496
- /* ignore */
497
- }
498
- result = await this.#waitForExitWithTimeout(timeoutMs);
499
- }
500
-
501
- const confirmed = !!result;
502
- this.#shutdownConfirmed = confirmed;
503
- this.#disposed = true;
504
- return { confirmed };
505
- }
506
-
507
- #abortPendingExecutions(reason: string, options?: { kernelKilled?: boolean }): void {
508
- if (this.#pending.size === 0) return;
509
- const pending = Array.from(this.#pending.values());
510
- this.#pending.clear();
511
- const kernelKilledDefault = options?.kernelKilled ?? false;
512
- for (const entry of pending) {
513
- if (entry.settled) continue;
514
- entry.settled = true;
515
- void entry.options?.onChunk?.(`[kernel] ${reason}\n`);
516
- entry.resolve({
517
- status: "error",
518
- cancelled: true,
519
- timedOut: entry.timedOut,
520
- stdinRequested: entry.stdinRequested,
521
- executionCount: entry.executionCount,
522
- error: entry.error,
523
- kernelKilled: entry.kernelKilled || kernelKilledDefault,
524
- });
525
- }
526
- }
527
-
528
- async #writeLine(line: string): Promise<void> {
529
- if (!this.#stdin) {
530
- throw new Error("Python kernel stdin is not open");
531
- }
532
- if (TRACE_IPC) {
533
- logger.debug("PythonKernel send", { preview: line.slice(0, 120) });
534
- }
535
- this.#stdin.write(`${line}\n`);
536
- this.#stdin.flush();
537
- }
538
-
539
- #startReader(stream: ReadableStream<Uint8Array>): void {
540
- const reader = stream.getReader();
541
- const decoder = new TextDecoder();
542
- const loop = async () => {
543
- try {
544
- while (true) {
545
- const { done, value } = await reader.read();
546
- if (done) break;
547
- this.#readBuffer += decoder.decode(value, { stream: true });
548
- await this.#flushFrames();
549
- }
550
- this.#readBuffer += decoder.decode();
551
- await this.#flushFrames();
552
- } catch (err) {
553
- logger.warn("Python kernel reader failed", { error: err instanceof Error ? err.message : String(err) });
554
- } finally {
555
- try {
556
- reader.releaseLock();
557
- } catch {
558
- /* ignore */
559
- }
560
- }
561
- };
562
- void loop();
563
- }
564
-
565
- #startStderrDrain(stream: ReadableStream<Uint8Array>): void {
566
- // Wrapper writes its own crashes to stderr; surface them via logger so the
567
- // host operator can debug runtime issues without polluting tool output.
568
- const reader = stream.getReader();
569
- const decoder = new TextDecoder();
570
- const loop = async () => {
571
- try {
572
- while (true) {
573
- const { done, value } = await reader.read();
574
- if (done) break;
575
- const text = decoder.decode(value);
576
- if (text.trim()) {
577
- logger.warn("Python runner stderr", { text });
578
- }
579
- }
580
- } catch {
581
- /* ignore */
582
- } finally {
583
- try {
584
- reader.releaseLock();
585
- } catch {
586
- /* ignore */
587
- }
588
- }
589
- };
590
- void loop();
591
- }
592
-
593
- async #flushFrames(): Promise<void> {
594
- while (true) {
595
- const nl = this.#readBuffer.indexOf("\n");
596
- if (nl < 0) return;
597
- const line = this.#readBuffer.slice(0, nl);
598
- this.#readBuffer = this.#readBuffer.slice(nl + 1);
599
- if (!line.trim()) continue;
600
- let frame: Frame;
601
- try {
602
- frame = JSON.parse(line) as Frame;
603
- } catch (err) {
604
- logger.warn("Python runner emitted invalid JSON", {
605
- line: line.slice(0, 200),
606
- error: err instanceof Error ? err.message : String(err),
607
- });
608
- continue;
609
- }
610
- if (TRACE_IPC) {
611
- logger.debug("PythonKernel recv", { type: frame.type, id: frame.id });
612
- }
613
- await this.#handleFrame(frame);
614
- }
615
- }
616
-
617
- async #handleFrame(frame: Frame): Promise<void> {
618
- const rid = frame.id;
619
- if (!rid) return;
620
- const pending = this.#pending.get(rid) as (PendingExecution & { finalize?: () => void }) | undefined;
621
- if (!pending) return;
622
-
623
- switch (frame.type) {
624
- case "started":
625
- return;
626
- case "stdout":
627
- case "stderr": {
628
- const text = frame.data ?? "";
629
- if (text && pending.options?.onChunk) {
630
- await pending.options.onChunk(text);
631
- }
632
- return;
633
- }
634
- case "display":
635
- case "result": {
636
- const bundle = frame.bundle ?? {};
637
- const { text, outputs } = await renderKernelDisplay(bundle);
638
- if (text && pending.options?.onChunk) {
639
- await pending.options.onChunk(text);
640
- }
641
- if (outputs.length > 0 && pending.options?.onDisplay) {
642
- for (const output of outputs) {
643
- await pending.options.onDisplay(output);
644
- }
645
- }
646
- return;
647
- }
648
- case "error": {
649
- const traceback = Array.isArray(frame.traceback) ? frame.traceback.map(String) : [];
650
- pending.status = "error";
651
- pending.error = {
652
- name: String(frame.ename ?? "Error"),
653
- value: String(frame.evalue ?? ""),
654
- traceback,
655
- };
656
- const message =
657
- traceback.length > 0 ? `${traceback.join("\n")}\n` : `${pending.error.name}: ${pending.error.value}\n`;
658
- if (pending.options?.onChunk) {
659
- await pending.options.onChunk(message);
660
- }
661
- return;
662
- }
663
- case "done": {
664
- if (typeof frame.executionCount === "number") {
665
- pending.executionCount = frame.executionCount;
666
- }
667
- if (frame.status === "error" && pending.status === "ok") {
668
- pending.status = "error";
669
- }
670
- if (frame.cancelled) {
671
- pending.cancelled = true;
672
- }
673
- pending.finalize?.();
674
- return;
675
- }
676
- }
677
- }
678
-
679
- async #executeWithBudget(
680
- code: string,
681
- signal: AbortSignal | undefined,
682
- timeoutMs: number,
683
- label: string,
684
- ): Promise<void> {
685
- const controller = new AbortController();
686
- const cleanups: Array<() => void> = [];
687
- if (signal) {
688
- if (signal.aborted) {
689
- controller.abort(signal.reason);
690
- } else {
691
- const onAbort = () => controller.abort(signal.reason);
692
- signal.addEventListener("abort", onAbort, { once: true });
693
- cleanups.push(() => signal.removeEventListener("abort", onAbort));
694
- }
695
- }
696
- const timer =
697
- timeoutMs > 0
698
- ? setTimeout(() => controller.abort(createAbortError("TimeoutError", `${label} timed out`)), timeoutMs)
699
- : undefined;
700
- if (timer) cleanups.push(() => clearTimeout(timer));
701
- try {
702
- throwIfAborted(controller.signal, label);
703
- const result = await this.execute(code, {
704
- signal: controller.signal,
705
- silent: true,
706
- storeHistory: false,
707
- });
708
- if (result.cancelled) {
709
- throw createAbortError(result.timedOut ? "TimeoutError" : "AbortError", `${label} cancelled`);
710
- }
711
- if (result.status === "error") {
712
- const reason = result.error?.value ?? "Python kernel init failed";
713
- throw new Error(`${label} failed: ${reason}`);
714
- }
715
- } finally {
716
- for (const cleanup of cleanups) cleanup();
717
- }
718
- }
719
-
720
- #waitForExitWithTimeout(timeoutMs: number): Promise<number | null> {
721
- if (!this.#exitedPromise) return Promise.resolve(0);
722
- const exitedPromise = this.#exitedPromise;
723
- const timeout = new Promise<null>(resolve => {
724
- const timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
725
- timer.unref?.();
726
- });
727
- return Promise.race([exitedPromise.then(code => code as number | null), timeout]);
728
- }
729
- }
730
-
731
- function isTimeoutReason(reason: unknown): boolean {
732
- if (reason instanceof DOMException) return reason.name === "TimeoutError";
733
- if (reason instanceof Error) return reason.name === "TimeoutError";
734
- return false;
735
221
  }
736
-
737
222
  function buildInitScript(cwd: string, env?: Record<string, string | undefined>): string {
738
223
  const envEntries = Object.entries(env ?? {}).filter(([, value]) => value !== undefined);
739
224
  const envPayload = Object.fromEntries(envEntries);