@oh-my-pi/pi-utils 16.1.2 → 16.1.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.3] - 2026-06-19
6
+
7
+ ### Changed
8
+
9
+ - Expanded the `TempDir` Windows retry window from 4×10ms to 40×25ms (1s total) to accommodate SQLite WAL/SHM file handle release delays
10
+
11
+ ### Fixed
12
+
13
+ - Made EPIPE rejections from IPC `send()` to worker subprocesses (`syscall: "send"`) non-fatal: the global `unhandledRejection` handler now logs and continues instead of terminating the session when an optional subsystem's pipe breaks. A broken optional subsystem (TTS/STT/tiny-title/MCP) can no longer crash the whole agent session mid-task. ([#2997](https://github.com/can1357/oh-my-pi/issues/2997))
14
+
5
15
  ## [16.1.2] - 2026-06-19
6
16
 
7
17
  ### Added
@@ -8,6 +8,14 @@ export declare enum Reason {
8
8
  UNHANDLED_REJECTION = "unhandled_rejection",// Unhandled promise rejection
9
9
  MANUAL = "manual"
10
10
  }
11
+ /**
12
+ * Detect an EPIPE rejection that originated from an IPC `send()` to a worker
13
+ * subprocess (`syscall: "send"`), as opposed to a stdin/stdout pipe write
14
+ * (`syscall: "write"`). Only the IPC-send path can break an optional worker
15
+ * subsystem without affecting the main process, so only this shape is safe to
16
+ * swallow at the global `unhandledRejection` level. See issue #2997.
17
+ */
18
+ export declare function isIpcSendEpipe(err: Error): boolean;
11
19
  /**
12
20
  * Register a process cleanup callback, to be run on shutdown, signal, or fatal error.
13
21
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "16.1.2",
4
+ "version": "16.1.3",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "16.1.2",
34
+ "@oh-my-pi/pi-natives": "16.1.3",
35
35
  "handlebars": "^4.7.9",
36
36
  "winston": "^3.19.0",
37
37
  "winston-daily-rotate-file": "^5.0.0"
package/src/postmortem.ts CHANGED
@@ -65,6 +65,19 @@ function runCleanup(reason: Reason): Promise<void> {
65
65
  // Worker thread: exit only (workers use self.addEventListener for exceptions)
66
66
  let inspectorOpened = false;
67
67
 
68
+ /**
69
+ * Detect an EPIPE rejection that originated from an IPC `send()` to a worker
70
+ * subprocess (`syscall: "send"`), as opposed to a stdin/stdout pipe write
71
+ * (`syscall: "write"`). Only the IPC-send path can break an optional worker
72
+ * subsystem without affecting the main process, so only this shape is safe to
73
+ * swallow at the global `unhandledRejection` level. See issue #2997.
74
+ */
75
+ export function isIpcSendEpipe(err: Error): boolean {
76
+ const code = (err as { code?: unknown }).code;
77
+ const syscall = (err as { syscall?: unknown }).syscall;
78
+ return code === "EPIPE" && syscall === "send";
79
+ }
80
+
68
81
  function formatFatalError(label: string, err: Error): string {
69
82
  const name = err.name || "Error";
70
83
  const message = err.message || "(no message)";
@@ -95,6 +108,19 @@ if (isMainThread) {
95
108
  })
96
109
  .on("unhandledRejection", async reason => {
97
110
  const err = reason instanceof Error ? reason : new Error(String(reason));
111
+ // EPIPE from an IPC `send()` (`syscall: "send"`) originates from a
112
+ // worker subprocess whose pipe broke between the exit being observed
113
+ // and the next `proc.send()` — a race window that Bun surfaces as an
114
+ // async rejection rather than the synchronous "cannot be used after
115
+ // the process has exited" guard. Every `send()` target is an optional
116
+ // worker subsystem (TTS, STT, tiny-title, MCP servers), so a broken
117
+ // send pipe must never take down the whole session. Log and continue
118
+ // instead of exiting; the owning client detects the dead worker via
119
+ // its own `onExit`/error path and respawns or disables it. See #2997.
120
+ if (isIpcSendEpipe(err)) {
121
+ logger.warn("Ignoring EPIPE from worker IPC send; optional subsystem will self-recover", { err });
122
+ return;
123
+ }
98
124
  process.stderr.write(formatFatalError("Unhandled Rejection", err));
99
125
  logger.error("Unhandled rejection", { err });
100
126
  await runCleanup(Reason.UNHANDLED_REJECTION);
package/src/temp.ts CHANGED
@@ -78,8 +78,8 @@ function normalizePrefix(prefix?: string): string {
78
78
  }
79
79
 
80
80
  const kRemoveOptions = { recursive: true, force: true } as const;
81
- const kRemoveRetries = 4;
82
- const kRemoveRetryDelayMs = 10;
81
+ const kRemoveRetries = 40;
82
+ const kRemoveRetryDelayMs = 25;
83
83
  const kRetryableRemoveErrorCodes = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
84
84
  const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
85
85