@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 +10 -0
- package/dist/types/postmortem.d.ts +8 -0
- package/package.json +2 -2
- package/src/postmortem.ts +26 -0
- package/src/temp.ts +2 -2
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.
|
|
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.
|
|
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 =
|
|
82
|
-
const kRemoveRetryDelayMs =
|
|
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
|
|