@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.2

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 (71) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +957 -214
  3. package/dist/types/config/model-registry.d.ts +1 -0
  4. package/dist/types/config/models-config-schema.d.ts +3 -0
  5. package/dist/types/config/models-config.d.ts +3 -0
  6. package/dist/types/config/settings-schema.d.ts +66 -0
  7. package/dist/types/edit/hashline/block-resolver.d.ts +1 -1
  8. package/dist/types/edit/index.d.ts +2 -0
  9. package/dist/types/modes/components/welcome.d.ts +1 -0
  10. package/dist/types/modes/controllers/input-controller.d.ts +4 -4
  11. package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
  12. package/dist/types/sdk.d.ts +3 -0
  13. package/dist/types/session/session-dump-format.d.ts +2 -1
  14. package/dist/types/system-prompt.d.ts +11 -0
  15. package/dist/types/tools/ask.d.ts +2 -0
  16. package/dist/types/tools/ast-edit.d.ts +2 -0
  17. package/dist/types/tools/ast-grep.d.ts +2 -0
  18. package/dist/types/tools/browser.d.ts +2 -0
  19. package/dist/types/tools/debug.d.ts +2 -0
  20. package/dist/types/tools/eval.d.ts +2 -0
  21. package/dist/types/tools/find.d.ts +2 -0
  22. package/dist/types/tools/inspect-image.d.ts +2 -1
  23. package/dist/types/tools/irc.d.ts +2 -0
  24. package/dist/types/tools/ssh.d.ts +2 -0
  25. package/dist/types/tools/todo.d.ts +2 -0
  26. package/dist/types/tui/tree-list.d.ts +1 -0
  27. package/package.json +12 -12
  28. package/src/config/model-registry.ts +10 -0
  29. package/src/config/models-config-schema.ts +2 -0
  30. package/src/config/models-config.ts +1 -0
  31. package/src/config/settings-schema.ts +53 -0
  32. package/src/edit/hashline/block-resolver.ts +1 -1
  33. package/src/edit/hashline/execute.ts +1 -6
  34. package/src/edit/index.ts +48 -0
  35. package/src/eval/__tests__/js-context-manager.test.ts +41 -1
  36. package/src/eval/js/context-manager.ts +92 -26
  37. package/src/eval/js/worker-core.ts +1 -1
  38. package/src/internal-urls/docs-index.generated.ts +9 -2
  39. package/src/modes/components/welcome.ts +14 -4
  40. package/src/modes/controllers/input-controller.ts +21 -38
  41. package/src/modes/rpc/rpc-mode.ts +1 -0
  42. package/src/modes/rpc/rpc-types.ts +2 -2
  43. package/src/prompts/system/system-prompt.md +17 -21
  44. package/src/prompts/tools/ask.md +0 -8
  45. package/src/prompts/tools/ast-edit.md +0 -15
  46. package/src/prompts/tools/ast-grep.md +0 -13
  47. package/src/prompts/tools/browser.md +0 -21
  48. package/src/prompts/tools/debug.md +0 -13
  49. package/src/prompts/tools/eval.md +0 -9
  50. package/src/prompts/tools/find.md +0 -13
  51. package/src/prompts/tools/inspect-image.md +0 -9
  52. package/src/prompts/tools/irc.md +0 -15
  53. package/src/prompts/tools/patch.md +0 -13
  54. package/src/prompts/tools/ssh.md +0 -9
  55. package/src/prompts/tools/todo.md +1 -19
  56. package/src/sdk.ts +19 -0
  57. package/src/session/agent-session.ts +125 -19
  58. package/src/session/session-dump-format.ts +10 -31
  59. package/src/system-prompt.ts +31 -0
  60. package/src/tools/ask.ts +41 -0
  61. package/src/tools/ast-edit.ts +46 -0
  62. package/src/tools/ast-grep.ts +24 -0
  63. package/src/tools/browser.ts +52 -0
  64. package/src/tools/debug.ts +17 -0
  65. package/src/tools/eval.ts +20 -1
  66. package/src/tools/find.ts +24 -0
  67. package/src/tools/inspect-image.ts +27 -1
  68. package/src/tools/irc.ts +41 -0
  69. package/src/tools/ssh.ts +16 -0
  70. package/src/tools/todo.ts +82 -3
  71. package/src/tui/tree-list.ts +68 -19
@@ -1719,6 +1719,48 @@ export const SETTINGS_SCHEMA = {
1719
1719
  },
1720
1720
  },
1721
1721
 
1722
+ "tools.format": {
1723
+ type: "enum",
1724
+ values: [
1725
+ "auto",
1726
+ "native",
1727
+ "glm",
1728
+ "hermes",
1729
+ "kimi",
1730
+ "xml",
1731
+ "anthropic",
1732
+ "deepseek",
1733
+ "harmony",
1734
+ "pi",
1735
+ "qwen3",
1736
+ ] as const,
1737
+ default: "auto",
1738
+ ui: {
1739
+ tab: "context",
1740
+ group: "Experimental",
1741
+ label: "Tool Call Format",
1742
+ description:
1743
+ "Controls how tools are exposed to the model. Auto uses native tool calls unless the selected model is marked as not supporting tools, then falls back to GLM-style in-band tool calls. Native forces provider-native tools; the other values force the named in-band syntax. Applies on session start.",
1744
+ options: [
1745
+ {
1746
+ value: "auto",
1747
+ label: "Auto",
1748
+ description: "Use native tool calls unless the model is known not to support them.",
1749
+ },
1750
+ { value: "native", label: "Native", description: "Use provider-native tool calls." },
1751
+ { value: "glm", label: "GLM", description: "Use GLM-style in-band tool calls." },
1752
+ { value: "hermes", label: "Hermes", description: "Use Hermes-style in-band tool calls." },
1753
+ { value: "kimi", label: "Kimi", description: "Use Kimi-style in-band tool calls." },
1754
+ { value: "xml", label: "XML", description: "Use generic XML in-band tool calls." },
1755
+ { value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
1756
+ { value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
1757
+ { value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
1758
+ { value: "pi", label: "Pi", description: "Use Pi-style in-band tool calls." },
1759
+ { value: "qwen3", label: "Qwen3", description: "Use Qwen3-style in-band tool calls." },
1760
+ ],
1761
+ },
1762
+ },
1763
+
1722
1764
  "snapcompact.shape": {
1723
1765
  type: "enum",
1724
1766
  values: ["auto", ...SHAPE_VARIANT_NAMES] as const,
@@ -3183,6 +3225,17 @@ export const SETTINGS_SCHEMA = {
3183
3225
  description: "Ask the agent to describe the intent of each tool call before executing it",
3184
3226
  },
3185
3227
  },
3228
+ "tools.abortOnFabricatedResult": {
3229
+ type: "boolean",
3230
+ default: true,
3231
+ ui: {
3232
+ tab: "tools",
3233
+ group: "Execution",
3234
+ label: "Abort On Fabricated Tool Result",
3235
+ description:
3236
+ "With in-band tool calls, stop the model immediately when it starts hallucinating a tool result mid-turn. Disable to let the model finish generating and discard the fabricated continuation instead.",
3237
+ },
3238
+ },
3186
3239
 
3187
3240
  "tools.maxTimeout": {
3188
3241
  type: "number",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tree-sitter-backed {@link BlockResolver} for the hashline `replace block N:`
2
+ * Tree-sitter-backed {@link BlockResolver} for the hashline block replace
3
3
  * operator. Bridges the pure hashline seam to the native `blockRangeAt`
4
4
  * primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
5
5
  * path and returns the 1-indexed line span of the syntactic block beginning on
@@ -98,12 +98,7 @@ interface RenderedSection {
98
98
  }
99
99
 
100
100
  function formatBlockResolution(resolution: BlockResolution): string {
101
- const op =
102
- resolution.op === "delete"
103
- ? "delete block"
104
- : resolution.op === "insert_after"
105
- ? "insert after block"
106
- : "replace block";
101
+ const op = resolution.op === "delete" ? "DEL.BLK" : resolution.op === "insert_after" ? "INS.BLK.POST" : "SWAP.BLK";
107
102
  const lines = resolution.end - resolution.start + 1;
108
103
  const span =
109
104
  resolution.start === resolution.end ? `line ${resolution.start}` : `lines ${resolution.start}-${resolution.end}`;
package/src/edit/index.ts CHANGED
@@ -2,7 +2,9 @@ import { MismatchError as HashlineMismatchError } from "@oh-my-pi/hashline";
2
2
  import hashlineGrammar from "@oh-my-pi/hashline/grammar.lark" with { type: "text" };
3
3
  import hashlineDescription from "@oh-my-pi/hashline/prompt.md" with { type: "text" };
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
5
6
  import { prompt } from "@oh-my-pi/pi-utils";
7
+ import type { z } from "zod/v4";
6
8
  import {
7
9
  createLspWritethrough,
8
10
  type FileDiagnosticsResult,
@@ -50,6 +52,7 @@ type EditParams = ReplaceParams | PatchParams | HashlineParams | ApplyPatchParam
50
52
  type EditModeDefinition = {
51
53
  description: (session: ToolSession) => string;
52
54
  parameters: TInput;
55
+ examples?: readonly ToolExample[];
53
56
  execute: (
54
57
  tool: EditTool,
55
58
  params: EditParams,
@@ -356,6 +359,10 @@ export class EditTool implements AgentTool<TInput> {
356
359
  return this.#getModeDefinition().parameters;
357
360
  }
358
361
 
362
+ get examples(): readonly ToolExample[] | undefined {
363
+ return this.#getModeDefinition().examples;
364
+ }
365
+
359
366
  /**
360
367
  * When in `apply_patch` mode, expose the Codex Lark grammar so providers
361
368
  * that support OpenAI-style custom tools can emit a grammar-constrained
@@ -404,6 +411,39 @@ export class EditTool implements AgentTool<TInput> {
404
411
  patch: {
405
412
  description: () => prompt.render(patchDescription),
406
413
  parameters: patchEditSchema,
414
+ examples: [
415
+ {
416
+ caption: "Create",
417
+ call: { path: "hello.txt", edits: [{ op: "create", diff: "Hello\n" }] },
418
+ },
419
+ {
420
+ caption: "Update",
421
+ call: {
422
+ path: "src/app.py",
423
+ edits: [
424
+ {
425
+ op: "update",
426
+ diff: "@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n",
427
+ },
428
+ ],
429
+ },
430
+ },
431
+ {
432
+ caption: "Rename",
433
+ call: {
434
+ path: "src/app.py",
435
+ edits: [{ op: "update", rename: "src/main.py", diff: "@@\n …\n" }],
436
+ },
437
+ },
438
+ {
439
+ caption: "Delete",
440
+ call: { path: "obsolete.txt", edits: [{ op: "delete" }] },
441
+ },
442
+ {
443
+ caption: "Multiple entries",
444
+ note: "All entries in one call apply to the top-level `path`; use separate calls for different files.",
445
+ },
446
+ ] satisfies readonly ToolExample<z.input<typeof patchEditSchema>>[],
407
447
  execute: (
408
448
  tool: EditTool,
409
449
  params: EditParams,
@@ -432,6 +472,14 @@ export class EditTool implements AgentTool<TInput> {
432
472
  apply_patch: {
433
473
  description: () => prompt.render(applyPatchDescription),
434
474
  parameters: applyPatchSchema,
475
+ examples: [
476
+ {
477
+ caption: "Apply a combined patch file",
478
+ call: {
479
+ input: '*** Begin Patch\n*** Add File: hello.txt\n+Hello world\n*** Update File: src/app.py\n*** Move to: src/main.py\n@@ def greet():\n-print("Hi")\n+print("Hello, world!")\n*** Delete File: obsolete.txt\n*** End Patch\n',
480
+ },
481
+ },
482
+ ] satisfies readonly ToolExample<z.input<typeof applyPatchSchema>>[],
435
483
  execute: (
436
484
  tool: EditTool,
437
485
  params: EditParams,
@@ -15,6 +15,7 @@ interface FakeWorkerStats {
15
15
  interface FakeWorkerBehavior {
16
16
  exitOnClose: boolean;
17
17
  settleRuns: boolean;
18
+ errorOnStart?: boolean;
18
19
  }
19
20
 
20
21
  function makeSession(cwd: string): ToolSession {
@@ -70,6 +71,7 @@ async function waitForRealWorkerExitAfterClose(cwd: string): Promise<void> {
70
71
  worker.addEventListener("close", () => workerClosed.resolve());
71
72
 
72
73
  try {
74
+ worker.postMessage({ type: "init", snapshot });
73
75
  await withTimeout(ready.promise, 1_000, "worker ready");
74
76
  worker.postMessage({
75
77
  type: "run",
@@ -91,6 +93,7 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
91
93
  class FakeWorker {
92
94
  #messageListeners = new Set<(event: MessageEvent) => void>();
93
95
  #closeListeners = new Set<(event: Event) => void>();
96
+ #errorListeners = new Set<(event: Event) => void>();
94
97
  #readyQueued = false;
95
98
  #exited = false;
96
99
 
@@ -115,11 +118,18 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
115
118
  this.#closeListeners.add(listener as (event: Event) => void);
116
119
  return;
117
120
  }
121
+ if (type === "error") {
122
+ this.#errorListeners.add(listener as (event: Event) => void);
123
+ return;
124
+ }
118
125
  if (type !== "message") return;
119
126
  this.#messageListeners.add(listener as (event: MessageEvent) => void);
120
127
  if (!this.#readyQueued) {
121
128
  this.#readyQueued = true;
122
- queueMicrotask(() => this.#emitMessage({ type: "ready" }));
129
+ queueMicrotask(() => {
130
+ if (behavior.errorOnStart) this.#emitError();
131
+ else this.#emitMessage({ type: "ready" });
132
+ });
123
133
  }
124
134
  }
125
135
 
@@ -128,6 +138,10 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
128
138
  this.#closeListeners.delete(listener as (event: Event) => void);
129
139
  return;
130
140
  }
141
+ if (type === "error") {
142
+ this.#errorListeners.delete(listener as (event: Event) => void);
143
+ return;
144
+ }
131
145
  if (type !== "message") return;
132
146
  this.#messageListeners.delete(listener as (event: MessageEvent) => void);
133
147
  }
@@ -148,6 +162,14 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
148
162
  const event = new Event("close");
149
163
  for (const listener of this.#closeListeners) listener(event);
150
164
  }
165
+
166
+ #emitError(): void {
167
+ const event = new ErrorEvent("error", {
168
+ message: "fake worker failed to start",
169
+ error: new Error("fake worker failed to start"),
170
+ });
171
+ for (const listener of this.#errorListeners) listener(event);
172
+ }
151
173
  }
152
174
 
153
175
  Object.defineProperty(globalThis, "Worker", {
@@ -238,4 +260,22 @@ describe("JavaScript eval worker lifecycle", () => {
238
260
  expect(stats.closeRequests).toBe(0);
239
261
  expect(stats.terminateCalls).toBe(1);
240
262
  });
263
+
264
+ it("falls back to the inline worker when the spawned worker errors during startup", async () => {
265
+ using tempDir = TempDir.createSync("@omp-js-worker-error-");
266
+ const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
267
+ installFakeWorker(stats, { exitOnClose: true, settleRuns: true, errorOnStart: true });
268
+
269
+ const session = makeSession(tempDir.path());
270
+ const sessionId = `js-worker-error:${crypto.randomUUID()}`;
271
+
272
+ // The spawned worker emits an `error` event instead of `ready`. Without fail-fast
273
+ // error handling the handshake would stall until WORKER_INIT_TIMEOUT_MS (15s); with
274
+ // it, the handshake rejects at once and the inline worker runs the cell.
275
+ const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
276
+ expect(result.exitCode).toBe(0);
277
+ expect(result.output.trim()).toBe("42");
278
+ // The errored primary worker is torn down before the inline retry takes over.
279
+ expect(stats.terminateCalls).toBe(1);
280
+ });
241
281
  });
@@ -27,6 +27,7 @@ interface WorkerHandle {
27
27
  mode: "worker" | "inline";
28
28
  send(msg: WorkerInbound): void;
29
29
  onMessage(handler: (msg: WorkerOutbound) => void): () => void;
30
+ onError(handler: (error: Error) => void): () => void;
30
31
  close(): Promise<boolean>;
31
32
  terminate(): Promise<void>;
32
33
  }
@@ -186,41 +187,45 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
186
187
  if (starting) return await starting;
187
188
 
188
189
  const startup = (async (): Promise<JsSession> => {
189
- const worker = await spawnJsWorker();
190
+ // The message listener must be attached synchronously after `new Worker`:
191
+ // Bun drops messages posted before a listener exists, and WorkerCore emits
192
+ // `ready` from its constructor on load. `spawnJsWorker` + `initWorker` run with
193
+ // no intervening await, so `ready` can never race the attach.
194
+ const worker = spawnJsWorker();
190
195
  const session: JsSession = {
191
196
  sessionKey,
192
197
  worker,
193
198
  state: "alive",
194
199
  pending: new Map(),
195
200
  };
196
- const { promise: readyPromise, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<void>();
197
- let resolved = false;
198
- const unsubscribe = worker.onMessage(msg => {
199
- if (!resolved && msg.type === "ready") {
200
- resolved = true;
201
- resolveReady();
202
- return;
203
- }
204
- if (!resolved && msg.type === "init-failed") {
205
- resolved = true;
206
- rejectReady(errorFromPayload(msg.error));
207
- return;
208
- }
209
- handleSessionMessage(session, msg);
210
- });
201
+ // Init headroom is the fixed infrastructure floor; the caller's per-cell timeout
202
+ // dominates when larger so users can grant more by raising `timeout` on a cell.
203
+ const readyTimeoutMs = Math.max(WORKER_INIT_TIMEOUT_MS, timeoutMs ?? 0);
211
204
  try {
212
- // Init headroom is the fixed infrastructure floor; the caller's per-cell timeout
213
- // dominates when larger so users can grant more by raising `timeout` on a cell.
214
- const readyTimeoutMs = Math.max(WORKER_INIT_TIMEOUT_MS, timeoutMs ?? 0);
215
- await raceWithTimeout(readyPromise, readyTimeoutMs, "Timed out initializing JS eval worker");
216
- worker.send({ type: "init", snapshot });
217
- sessions.set(sessionKey, session);
218
- return session;
205
+ await initWorker(session, snapshot, readyTimeoutMs);
219
206
  } catch (error) {
220
- unsubscribe();
207
+ // Worker-thread crash/load failures surface asynchronously via the worker
208
+ // `error` event — after `spawnJsWorker`'s synchronous try/catch already
209
+ // returned — so the only signal is the rejected handshake. Retry on the
210
+ // inline worker so a broken module graph fails fast instead of stalling
211
+ // every cell on the init timeout and then dying with exitCode 1.
221
212
  await worker.terminate().catch(() => undefined);
222
- throw error;
213
+ if (worker.mode === "inline") throw error;
214
+ logger.warn("JS eval worker init failed; retrying with inline worker (no sync-loop guard)", {
215
+ error: error instanceof Error ? error.message : String(error),
216
+ });
217
+ const inline = spawnInlineWorker();
218
+ session.worker = inline;
219
+ session.state = "alive";
220
+ try {
221
+ await initWorker(session, snapshot, readyTimeoutMs);
222
+ } catch (inlineError) {
223
+ await inline.terminate().catch(() => undefined);
224
+ throw inlineError;
225
+ }
223
226
  }
227
+ sessions.set(sessionKey, session);
228
+ return session;
224
229
  })();
225
230
  startingSessions.set(sessionKey, startup);
226
231
  try {
@@ -230,6 +235,49 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
230
235
  }
231
236
  }
232
237
 
238
+ async function initWorker(session: JsSession, snapshot: SessionSnapshot, timeoutMs: number): Promise<void> {
239
+ const worker = session.worker;
240
+ const { promise: readyPromise, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<void>();
241
+ let resolved = false;
242
+ const unsubscribeMessage = worker.onMessage(msg => {
243
+ if (!resolved && msg.type === "ready") {
244
+ resolved = true;
245
+ resolveReady();
246
+ return;
247
+ }
248
+ if (!resolved && msg.type === "init-failed") {
249
+ resolved = true;
250
+ rejectReady(errorFromPayload(msg.error));
251
+ return;
252
+ }
253
+ handleSessionMessage(session, msg);
254
+ });
255
+ const unsubscribeError = worker.onError(error => {
256
+ if (!resolved) {
257
+ resolved = true;
258
+ rejectReady(error);
259
+ return;
260
+ }
261
+ // Worker died after a successful handshake: tear the session down so the
262
+ // in-flight run (and the next acquire) fail fast instead of hanging on a
263
+ // worker that will never reply.
264
+ void killSessionFor(session, error, { force: true });
265
+ });
266
+ try {
267
+ // Attach listeners and send init before awaiting ready. The worker now
268
+ // emits ready only in response to init, so this ordering is race-free.
269
+ worker.send({ type: "init", snapshot });
270
+ await raceWithTimeout(readyPromise, timeoutMs, "Timed out initializing JS eval worker");
271
+ } catch (error) {
272
+ // Handshake failed (timeout, init-failed, or worker error): drop both listeners
273
+ // so the abandoned worker can't keep routing messages into a session the caller
274
+ // is about to discard or retry on the inline fallback.
275
+ unsubscribeMessage();
276
+ unsubscribeError();
277
+ throw error;
278
+ }
279
+ }
280
+
233
281
  function handleSessionMessage(session: JsSession, msg: WorkerOutbound): void {
234
282
  switch (msg.type) {
235
283
  case "text": {
@@ -379,7 +427,7 @@ async function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, reason
379
427
  }
380
428
  }
381
429
 
382
- async function spawnJsWorker(): Promise<WorkerHandle> {
430
+ function spawnJsWorker(): WorkerHandle {
383
431
  try {
384
432
  const hostEntry = workerHostEntry();
385
433
  const worker = hostEntry
@@ -405,6 +453,17 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
405
453
  worker.addEventListener("message", wrap);
406
454
  return () => worker.removeEventListener("message", wrap);
407
455
  },
456
+ onError(handler) {
457
+ const onError = (event: ErrorEvent): void => handler(errorFromWorkerEvent(event));
458
+ const onMessageError = (event: MessageEvent): void =>
459
+ handler(new ToolError(`JS eval worker message error: ${String(event.data)}`));
460
+ worker.addEventListener("error", onError);
461
+ worker.addEventListener("messageerror", onMessageError);
462
+ return () => {
463
+ worker.removeEventListener("error", onError);
464
+ worker.removeEventListener("messageerror", onMessageError);
465
+ };
466
+ },
408
467
  async close() {
409
468
  const { promise: closed, resolve } = Promise.withResolvers<boolean>();
410
469
  let settled = false;
@@ -443,6 +502,12 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
443
502
  };
444
503
  }
445
504
 
505
+ function errorFromWorkerEvent(event: ErrorEvent): Error {
506
+ if (event.error instanceof Error) return event.error;
507
+ if (event.message) return new Error(event.message);
508
+ return new Error("Unknown JS eval worker error");
509
+ }
510
+
446
511
  /**
447
512
  * Inline fallback for environments where Bun cannot spawn the worker entry
448
513
  * (e.g. some test runners). Preserves behavior but cannot interrupt synchronous
@@ -473,6 +538,7 @@ function spawnInlineWorker(): WorkerHandle {
473
538
  hostListeners.add(handler);
474
539
  return () => hostListeners.delete(handler);
475
540
  },
541
+ onError: () => () => {},
476
542
  async close() {
477
543
  const { promise: closed, resolve } = Promise.withResolvers<boolean>();
478
544
  let settled = false;
@@ -43,13 +43,13 @@ export class WorkerCore {
43
43
  constructor(transport: Transport) {
44
44
  this.#transport = transport;
45
45
  this.#unsubscribe = transport.onMessage(msg => this.#handle(msg));
46
- transport.send({ type: "ready" });
47
46
  }
48
47
 
49
48
  #handle(msg: WorkerInbound): void {
50
49
  switch (msg.type) {
51
50
  case "init":
52
51
  this.#ensureRuntime(msg.snapshot);
52
+ this.#transport.send({ type: "ready" });
53
53
  return;
54
54
  case "run":
55
55
  void this.#runOne(msg.runId, msg.code, msg.filename, msg.snapshot);