@oh-my-pi/pi-utils 17.4.1 → 17.4.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.
- package/CHANGELOG.md +6 -0
- package/dist/types/postmortem.d.ts +33 -0
- package/package.json +2 -2
- package/src/postmortem.ts +74 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.4.2] - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Made malformed advanced-serialization frames from a worker subprocess non-fatal: Bun surfaces an undecodable IPC frame as a process-level `uncaughtException` in the parent (oven-sh/bun#37287), which the postmortem handler treated as fatal and tore down every active session and subagent. The handler now recognizes the decode failure and, keeping the session alive, faults the active advanced-IPC worker subsystems so their clients reject in-flight requests and recycle the subprocess instead of awaiting forever — mirroring the existing ipc-send EPIPE containment. ([#9158](https://github.com/can1357/oh-my-pi/issues/9158))
|
|
10
|
+
|
|
5
11
|
## [17.4.1] - 2026-08-21
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -44,6 +44,39 @@ export type BrokenPipeSource = "ipc-send" | "stdio-write";
|
|
|
44
44
|
export declare function classifyBrokenPipe(err: Error): BrokenPipeSource | undefined;
|
|
45
45
|
/** Whether an EPIPE came from an IPC `send()` to an optional worker. */
|
|
46
46
|
export declare function isIpcSendEpipe(err: Error): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Detect Bun's advanced-serialization (structured-clone) IPC decode failure.
|
|
49
|
+
*
|
|
50
|
+
* When a worker subprocess spawned with `serialization: "advanced"` sends a
|
|
51
|
+
* malformed or truncated frame, Bun raises the decode failure as a
|
|
52
|
+
* process-level `uncaughtException` in the *parent* rather than routing it to
|
|
53
|
+
* the channel's `ipc()` callback (oven-sh/bun#37287). The error is a bare
|
|
54
|
+
* `TypeError: Unable to deserialize data.` whose only own property is `message`
|
|
55
|
+
* — it carries no `code`, no `syscall`, and no `stack`. Matching all four traits
|
|
56
|
+
* keeps unrelated application `TypeError`s (which always carry a populated
|
|
57
|
+
* multi-frame stack) on the fatal path, so a genuine bug is never silently
|
|
58
|
+
* swallowed.
|
|
59
|
+
*
|
|
60
|
+
* Every advanced-serialization channel in this process is an optional worker
|
|
61
|
+
* subsystem (TTS, STT, tiny-title, mnemopi embeddings, JS eval), so one
|
|
62
|
+
* worker's bad frame must fault only that worker — via its own `onExit`/error
|
|
63
|
+
* path — never tear down the whole session. Callers log-and-continue instead of
|
|
64
|
+
* taking the fatal path. Mirrors {@link classifyBrokenPipe} for the send side
|
|
65
|
+
* (#2997, #9158).
|
|
66
|
+
*/
|
|
67
|
+
export declare function isWorkerIpcDeserializeError(err: unknown): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Register a fault/recycle callback for an active advanced-serialization worker
|
|
70
|
+
* IPC channel.
|
|
71
|
+
*
|
|
72
|
+
* Bun surfaces a malformed frame as a process-global `uncaughtException`
|
|
73
|
+
* ({@link isWorkerIpcDeserializeError}) with no way to attribute it to a
|
|
74
|
+
* specific channel, so when one fires every registered handler is invoked to
|
|
75
|
+
* conservatively fault its worker — reject in-flight requests and recycle the
|
|
76
|
+
* subprocess — instead of leaving pending work to await forever. Returns an
|
|
77
|
+
* unregister function; callers MUST unregister when the worker exits.
|
|
78
|
+
*/
|
|
79
|
+
export declare function registerWorkerIpcFaultHandler(handler: (err: Error) => void): () => void;
|
|
47
80
|
/**
|
|
48
81
|
* Treat unhandled stdout EPIPE rejections as a graceful peer disconnect.
|
|
49
82
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.4.
|
|
4
|
+
"version": "17.4.2",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.4.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.4.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/postmortem.ts
CHANGED
|
@@ -146,6 +146,66 @@ export function isIpcSendEpipe(err: Error): boolean {
|
|
|
146
146
|
return classifyBrokenPipe(err) === "ipc-send";
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Detect Bun's advanced-serialization (structured-clone) IPC decode failure.
|
|
151
|
+
*
|
|
152
|
+
* When a worker subprocess spawned with `serialization: "advanced"` sends a
|
|
153
|
+
* malformed or truncated frame, Bun raises the decode failure as a
|
|
154
|
+
* process-level `uncaughtException` in the *parent* rather than routing it to
|
|
155
|
+
* the channel's `ipc()` callback (oven-sh/bun#37287). The error is a bare
|
|
156
|
+
* `TypeError: Unable to deserialize data.` whose only own property is `message`
|
|
157
|
+
* — it carries no `code`, no `syscall`, and no `stack`. Matching all four traits
|
|
158
|
+
* keeps unrelated application `TypeError`s (which always carry a populated
|
|
159
|
+
* multi-frame stack) on the fatal path, so a genuine bug is never silently
|
|
160
|
+
* swallowed.
|
|
161
|
+
*
|
|
162
|
+
* Every advanced-serialization channel in this process is an optional worker
|
|
163
|
+
* subsystem (TTS, STT, tiny-title, mnemopi embeddings, JS eval), so one
|
|
164
|
+
* worker's bad frame must fault only that worker — via its own `onExit`/error
|
|
165
|
+
* path — never tear down the whole session. Callers log-and-continue instead of
|
|
166
|
+
* taking the fatal path. Mirrors {@link classifyBrokenPipe} for the send side
|
|
167
|
+
* (#2997, #9158).
|
|
168
|
+
*/
|
|
169
|
+
export function isWorkerIpcDeserializeError(err: unknown): boolean {
|
|
170
|
+
return (
|
|
171
|
+
err instanceof TypeError &&
|
|
172
|
+
err.message === "Unable to deserialize data." &&
|
|
173
|
+
!err.stack &&
|
|
174
|
+
!("code" in err) &&
|
|
175
|
+
!("syscall" in err)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Recycle callbacks for the active advanced-serialization worker IPC channels. */
|
|
180
|
+
const workerIpcFaultHandlers = new Set<(err: Error) => void>();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Register a fault/recycle callback for an active advanced-serialization worker
|
|
184
|
+
* IPC channel.
|
|
185
|
+
*
|
|
186
|
+
* Bun surfaces a malformed frame as a process-global `uncaughtException`
|
|
187
|
+
* ({@link isWorkerIpcDeserializeError}) with no way to attribute it to a
|
|
188
|
+
* specific channel, so when one fires every registered handler is invoked to
|
|
189
|
+
* conservatively fault its worker — reject in-flight requests and recycle the
|
|
190
|
+
* subprocess — instead of leaving pending work to await forever. Returns an
|
|
191
|
+
* unregister function; callers MUST unregister when the worker exits.
|
|
192
|
+
*/
|
|
193
|
+
export function registerWorkerIpcFaultHandler(handler: (err: Error) => void): () => void {
|
|
194
|
+
workerIpcFaultHandlers.add(handler);
|
|
195
|
+
return () => workerIpcFaultHandlers.delete(handler);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Invoke every registered worker IPC fault handler, isolating handler throws. */
|
|
199
|
+
function faultWorkerIpcChannels(err: Error): void {
|
|
200
|
+
for (const handler of workerIpcFaultHandlers) {
|
|
201
|
+
try {
|
|
202
|
+
handler(err);
|
|
203
|
+
} catch (handlerErr) {
|
|
204
|
+
logger.warn("Worker IPC fault handler threw", { err: handlerErr });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
149
209
|
/**
|
|
150
210
|
* Treat unhandled stdout EPIPE rejections as a graceful peer disconnect.
|
|
151
211
|
*
|
|
@@ -283,6 +343,20 @@ if (isMainThread) {
|
|
|
283
343
|
logger.warn("Ignoring expected cleanup exception", { err });
|
|
284
344
|
return;
|
|
285
345
|
}
|
|
346
|
+
// A malformed advanced-serialization frame from a worker subprocess
|
|
347
|
+
// surfaces here as a process-level uncaughtException (oven-sh/bun#37287)
|
|
348
|
+
// rather than in the channel's ipc() callback, and Bun gives no way to
|
|
349
|
+
// tell which channel produced it. Contain it to the worker layer: keep
|
|
350
|
+
// the session alive and conservatively fault every active advanced-IPC
|
|
351
|
+
// worker so its owning client rejects in-flight requests and recycles
|
|
352
|
+
// the subprocess — a worker that sent a bad frame but stays alive would
|
|
353
|
+
// otherwise never fire onExit and leave callers awaiting forever.
|
|
354
|
+
// Mirrors the ipc-send EPIPE containment below (#9158, #2997).
|
|
355
|
+
if (isWorkerIpcDeserializeError(err)) {
|
|
356
|
+
logger.warn("Malformed worker IPC frame; faulting active worker subsystems", { err });
|
|
357
|
+
faultWorkerIpcChannels(err);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
286
360
|
await exitAfterFatal("Uncaught Exception", "Uncaught exception", err, Reason.UNCAUGHT_EXCEPTION);
|
|
287
361
|
})
|
|
288
362
|
.on("unhandledRejection", async reason => {
|