@unlinearity/cli-wechat-bridge 1.0.0
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/LICENSE.txt +21 -0
- package/README.md +531 -0
- package/bin/_run-entry.mjs +41 -0
- package/bin/wechat-bridge-claude.mjs +5 -0
- package/bin/wechat-bridge-codex.mjs +5 -0
- package/bin/wechat-bridge-opencode.mjs +5 -0
- package/bin/wechat-bridge-shell.mjs +5 -0
- package/bin/wechat-bridge.mjs +5 -0
- package/bin/wechat-check-update.mjs +5 -0
- package/bin/wechat-claude-start.mjs +5 -0
- package/bin/wechat-claude.mjs +5 -0
- package/bin/wechat-codex-start.mjs +5 -0
- package/bin/wechat-codex.mjs +5 -0
- package/bin/wechat-opencode-start.mjs +5 -0
- package/bin/wechat-opencode.mjs +5 -0
- package/package.json +86 -0
- package/src/bridge/bridge-adapters.claude.ts +1047 -0
- package/src/bridge/bridge-adapters.codex.ts +2733 -0
- package/src/bridge/bridge-adapters.core.ts +877 -0
- package/src/bridge/bridge-adapters.opencode.ts +2390 -0
- package/src/bridge/bridge-adapters.shared.ts +1647 -0
- package/src/bridge/bridge-adapters.shell.ts +456 -0
- package/src/bridge/bridge-adapters.ts +30 -0
- package/src/bridge/bridge-controller.ts +58 -0
- package/src/bridge/bridge-final-reply.ts +56 -0
- package/src/bridge/bridge-process-reaper.ts +417 -0
- package/src/bridge/bridge-state.ts +520 -0
- package/src/bridge/bridge-types.ts +218 -0
- package/src/bridge/bridge-utils.ts +1524 -0
- package/src/bridge/claude-hook.ts +92 -0
- package/src/bridge/claude-hooks.ts +376 -0
- package/src/bridge/wechat-bridge.ts +1305 -0
- package/src/commands/check-update.ts +46 -0
- package/src/companion/codex-panel-link.ts +151 -0
- package/src/companion/codex-panel.ts +225 -0
- package/src/companion/codex-remote-client.ts +178 -0
- package/src/companion/local-companion-link.ts +338 -0
- package/src/companion/local-companion-start.ts +520 -0
- package/src/companion/local-companion.ts +532 -0
- package/src/media/media-types.ts +11 -0
- package/src/runtime/create-runtime-host.ts +16 -0
- package/src/runtime/legacy-adapter-runtime.ts +55 -0
- package/src/runtime/runtime-types.ts +51 -0
- package/src/utils/version-checker.ts +197 -0
- package/src/wechat/channel-config.ts +141 -0
- package/src/wechat/setup.ts +232 -0
- package/src/wechat/standalone-bot.ts +483 -0
- package/src/wechat/wechat-channel.ts +608 -0
- package/src/wechat/wechat-transport.ts +1290 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type BridgeProcessRecord = {
|
|
4
|
+
pid: number;
|
|
5
|
+
parentPid?: number;
|
|
6
|
+
name?: string;
|
|
7
|
+
commandLine: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
|
|
11
|
+
const PEER_BRIDGE_EXIT_POLL_MS = 100;
|
|
12
|
+
|
|
13
|
+
function isPidAlive(pid: number): boolean {
|
|
14
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sleep(ms: number): Promise<void> {
|
|
27
|
+
if (ms <= 0) {
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isWechatBridgeCommandLine(commandLine: string): boolean {
|
|
38
|
+
return (
|
|
39
|
+
/wechat-bridge\.(?:ts|js|mjs)/i.test(commandLine) &&
|
|
40
|
+
/(?:^|\s)--adapter(?:\s|=)/i.test(commandLine)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detects `opencode serve` command lines spawned by the bridge adapter.
|
|
46
|
+
* Matches patterns like:
|
|
47
|
+
* opencode serve --port 12345 --hostname 127.0.0.1
|
|
48
|
+
* opencode.exe serve --port 12345
|
|
49
|
+
* Also matches wrapper invocations (opencode.cmd / opencode.bat) on Windows.
|
|
50
|
+
*/
|
|
51
|
+
export function isOpencodeServeCommandLine(commandLine: string): boolean {
|
|
52
|
+
return /\bopencode(?:\.exe|\.cmd|\.bat)?\b.*\bserve\b/i.test(commandLine);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detects `opencode attach` command lines spawned by the local OpenCode companion.
|
|
57
|
+
* Matches patterns like:
|
|
58
|
+
* opencode attach http://127.0.0.1:12345
|
|
59
|
+
* opencode.exe attach http://127.0.0.1:12345 --session ses_123
|
|
60
|
+
*/
|
|
61
|
+
export function isOpencodeAttachCommandLine(commandLine: string): boolean {
|
|
62
|
+
return /\bopencode(?:\.exe|\.cmd|\.bat)?\b.*\battach\b/i.test(commandLine);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeBridgeProcessRecord(value: unknown): BridgeProcessRecord | null {
|
|
66
|
+
if (!isRecord(value)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pid =
|
|
71
|
+
typeof value.ProcessId === "number"
|
|
72
|
+
? value.ProcessId
|
|
73
|
+
: typeof value.pid === "number"
|
|
74
|
+
? value.pid
|
|
75
|
+
: Number.NaN;
|
|
76
|
+
const commandLine =
|
|
77
|
+
typeof value.CommandLine === "string"
|
|
78
|
+
? value.CommandLine
|
|
79
|
+
: typeof value.commandLine === "string"
|
|
80
|
+
? value.commandLine
|
|
81
|
+
: "";
|
|
82
|
+
if (!Number.isInteger(pid) || pid <= 0 || !commandLine) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const record: BridgeProcessRecord = {
|
|
87
|
+
pid,
|
|
88
|
+
commandLine,
|
|
89
|
+
};
|
|
90
|
+
const parentPid =
|
|
91
|
+
typeof value.ParentProcessId === "number"
|
|
92
|
+
? value.ParentProcessId
|
|
93
|
+
: typeof value.parentPid === "number"
|
|
94
|
+
? value.parentPid
|
|
95
|
+
: undefined;
|
|
96
|
+
if (typeof parentPid === "number" && Number.isInteger(parentPid) && parentPid > 0) {
|
|
97
|
+
record.parentPid = parentPid;
|
|
98
|
+
}
|
|
99
|
+
const name =
|
|
100
|
+
typeof value.Name === "string"
|
|
101
|
+
? value.Name
|
|
102
|
+
: typeof value.name === "string"
|
|
103
|
+
? value.name
|
|
104
|
+
: undefined;
|
|
105
|
+
if (name) {
|
|
106
|
+
record.name = name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function parseWindowsBridgeProcessProbeOutput(
|
|
113
|
+
stdout: string,
|
|
114
|
+
currentPid = process.pid,
|
|
115
|
+
): BridgeProcessRecord[] {
|
|
116
|
+
const trimmed = stdout.trim();
|
|
117
|
+
if (!trimmed) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let parsed: unknown;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(trimmed);
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
129
|
+
return values
|
|
130
|
+
.map(normalizeBridgeProcessRecord)
|
|
131
|
+
.filter((record): record is BridgeProcessRecord => Boolean(record))
|
|
132
|
+
.filter(
|
|
133
|
+
(record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parsePosixBridgeProcessProbeOutput(
|
|
138
|
+
stdout: string,
|
|
139
|
+
currentPid = process.pid,
|
|
140
|
+
): BridgeProcessRecord[] {
|
|
141
|
+
return stdout
|
|
142
|
+
.split(/\r?\n/)
|
|
143
|
+
.map((line) => line.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((line) => {
|
|
146
|
+
const match = /^(\d+)\s+(.*)$/.exec(line);
|
|
147
|
+
if (!match) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const pid = Number(match[1]);
|
|
151
|
+
const commandLine = match[2] ?? "";
|
|
152
|
+
if (!Number.isInteger(pid) || pid <= 0 || !commandLine) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
pid,
|
|
157
|
+
commandLine,
|
|
158
|
+
} satisfies BridgeProcessRecord;
|
|
159
|
+
})
|
|
160
|
+
.filter((record): record is BridgeProcessRecord => Boolean(record))
|
|
161
|
+
.filter(
|
|
162
|
+
(record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function listWindowsBridgeProcesses(currentPid = process.pid): BridgeProcessRecord[] {
|
|
167
|
+
const probe = spawnSync(
|
|
168
|
+
"powershell.exe",
|
|
169
|
+
[
|
|
170
|
+
"-NoLogo",
|
|
171
|
+
"-NoProfile",
|
|
172
|
+
"-NonInteractive",
|
|
173
|
+
"-ExecutionPolicy",
|
|
174
|
+
"Bypass",
|
|
175
|
+
"-Command",
|
|
176
|
+
[
|
|
177
|
+
"$ErrorActionPreference='Stop'",
|
|
178
|
+
"Get-CimInstance Win32_Process",
|
|
179
|
+
"| Where-Object { $_.CommandLine }",
|
|
180
|
+
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
181
|
+
"| ConvertTo-Json -Compress",
|
|
182
|
+
].join(" "),
|
|
183
|
+
],
|
|
184
|
+
{
|
|
185
|
+
encoding: "utf8",
|
|
186
|
+
windowsHide: true,
|
|
187
|
+
timeout: 8_000,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return parseWindowsBridgeProcessProbeOutput(probe.stdout, currentPid);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function listPosixBridgeProcesses(currentPid = process.pid): BridgeProcessRecord[] {
|
|
199
|
+
const probe = spawnSync(
|
|
200
|
+
"ps",
|
|
201
|
+
["-ax", "-o", "pid=", "-o", "command="],
|
|
202
|
+
{
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
timeout: 8_000,
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return parsePosixBridgeProcessProbeOutput(probe.stdout, currentPid);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function listPeerBridgeProcesses(currentPid = process.pid): BridgeProcessRecord[] {
|
|
216
|
+
return process.platform === "win32"
|
|
217
|
+
? listWindowsBridgeProcesses(currentPid)
|
|
218
|
+
: listPosixBridgeProcesses(currentPid);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* List all managed OpenCode child processes (`serve` and `attach`) whose parent
|
|
223
|
+
* PID is no longer alive. These are orphaned processes left behind when a bridge
|
|
224
|
+
* or local companion crashed or was killed without cleaning up its child
|
|
225
|
+
* OpenCode processes.
|
|
226
|
+
*/
|
|
227
|
+
export function listOrphanedOpencodeProcesses(
|
|
228
|
+
currentPid = process.pid,
|
|
229
|
+
): BridgeProcessRecord[] {
|
|
230
|
+
const all = listAllProcessesRaw(currentPid);
|
|
231
|
+
return all.filter((record) => {
|
|
232
|
+
if (
|
|
233
|
+
!isOpencodeServeCommandLine(record.commandLine) &&
|
|
234
|
+
!isOpencodeAttachCommandLine(record.commandLine)
|
|
235
|
+
) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (!record.parentPid) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (record.parentPid === currentPid) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return !isPidAlive(record.parentPid);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function listAllProcessesRaw(currentPid = process.pid): BridgeProcessRecord[] {
|
|
249
|
+
if (process.platform === "win32") {
|
|
250
|
+
const probe = spawnSync(
|
|
251
|
+
"powershell.exe",
|
|
252
|
+
[
|
|
253
|
+
"-NoLogo",
|
|
254
|
+
"-NoProfile",
|
|
255
|
+
"-NonInteractive",
|
|
256
|
+
"-ExecutionPolicy",
|
|
257
|
+
"Bypass",
|
|
258
|
+
"-Command",
|
|
259
|
+
[
|
|
260
|
+
"$ErrorActionPreference='Stop'",
|
|
261
|
+
"Get-CimInstance Win32_Process",
|
|
262
|
+
"| Where-Object { $_.CommandLine }",
|
|
263
|
+
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
264
|
+
"| ConvertTo-Json -Compress",
|
|
265
|
+
].join(" "),
|
|
266
|
+
],
|
|
267
|
+
{
|
|
268
|
+
encoding: "utf8",
|
|
269
|
+
windowsHide: true,
|
|
270
|
+
timeout: 8_000,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const trimmed = probe.stdout.trim();
|
|
279
|
+
if (!trimmed) return [];
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(trimmed);
|
|
282
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
283
|
+
return values
|
|
284
|
+
.map(normalizeBridgeProcessRecord)
|
|
285
|
+
.filter((r): r is BridgeProcessRecord => Boolean(r))
|
|
286
|
+
.filter((r) => r.pid !== currentPid);
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const probe = spawnSync("ps", ["-ax", "-o", "pid=", "-o", "ppid=", "-o", "command="], {
|
|
293
|
+
encoding: "utf8",
|
|
294
|
+
timeout: 8_000,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return probe.stdout
|
|
302
|
+
.split(/\r?\n/)
|
|
303
|
+
.map((line) => line.trim())
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
.map((line) => {
|
|
306
|
+
const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
307
|
+
if (!match) return null;
|
|
308
|
+
const pid = Number(match[1]);
|
|
309
|
+
const parentPid = Number(match[2]);
|
|
310
|
+
const commandLine = match[3] ?? "";
|
|
311
|
+
if (!Number.isInteger(pid) || pid <= 0 || !commandLine) return null;
|
|
312
|
+
const record: BridgeProcessRecord = { pid, commandLine };
|
|
313
|
+
if (Number.isInteger(parentPid) && parentPid > 0) record.parentPid = parentPid;
|
|
314
|
+
return record;
|
|
315
|
+
})
|
|
316
|
+
.filter((r): r is BridgeProcessRecord => Boolean(r))
|
|
317
|
+
.filter((r) => r.pid !== currentPid);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
321
|
+
const deadline = Date.now() + timeoutMs;
|
|
322
|
+
while (Date.now() < deadline) {
|
|
323
|
+
if (!isPidAlive(pid)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
await sleep(Math.min(PEER_BRIDGE_EXIT_POLL_MS, deadline - Date.now()));
|
|
327
|
+
}
|
|
328
|
+
return !isPidAlive(pid);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Synchronously kill a process and its entire descendant tree.
|
|
333
|
+
* On Windows uses `taskkill /T /F /PID` to recursively kill all descendants.
|
|
334
|
+
* On other platforms uses `process.kill(pid)` directly.
|
|
335
|
+
*/
|
|
336
|
+
export function killProcessTreeSync(pid: number): void {
|
|
337
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (process.platform === "win32") {
|
|
342
|
+
try {
|
|
343
|
+
spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], {
|
|
344
|
+
windowsHide: true,
|
|
345
|
+
timeout: 5_000,
|
|
346
|
+
});
|
|
347
|
+
} catch {
|
|
348
|
+
try { process.kill(pid); } catch { /* best effort */ }
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
try { process.kill(pid); } catch { /* best effort */ }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function reapPeerBridgeProcesses(params: {
|
|
356
|
+
currentPid?: number;
|
|
357
|
+
logger?: (message: string) => void;
|
|
358
|
+
} = {}): Promise<number[]> {
|
|
359
|
+
const currentPid = params.currentPid ?? process.pid;
|
|
360
|
+
const peers = listPeerBridgeProcesses(currentPid);
|
|
361
|
+
const terminated: number[] = [];
|
|
362
|
+
|
|
363
|
+
for (const peer of peers) {
|
|
364
|
+
try {
|
|
365
|
+
params.logger?.(
|
|
366
|
+
`peer_bridge_reap_attempt: pid=${peer.pid}${peer.name ? ` name=${peer.name}` : ""} command=${peer.commandLine}`,
|
|
367
|
+
);
|
|
368
|
+
killProcessTreeSync(peer.pid);
|
|
369
|
+
if (await waitForProcessExit(peer.pid, PEER_BRIDGE_EXIT_TIMEOUT_MS)) {
|
|
370
|
+
terminated.push(peer.pid);
|
|
371
|
+
params.logger?.(`peer_bridge_reaped: pid=${peer.pid}`);
|
|
372
|
+
} else {
|
|
373
|
+
params.logger?.(`peer_bridge_reap_timeout: pid=${peer.pid}`);
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
+
params.logger?.(`peer_bridge_reap_failed: pid=${peer.pid} error=${message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return terminated;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Kill orphaned managed OpenCode processes (`serve` and `attach`) whose parent
|
|
386
|
+
* bridge/companion has already exited. These accumulate when a bridge crashes
|
|
387
|
+
* or is killed without cleaning up its child OpenCode processes (common on
|
|
388
|
+
* Windows where child processes are not automatically killed when the parent dies).
|
|
389
|
+
*/
|
|
390
|
+
export async function reapOrphanedOpencodeProcesses(params: {
|
|
391
|
+
currentPid?: number;
|
|
392
|
+
logger?: (message: string) => void;
|
|
393
|
+
} = {}): Promise<number[]> {
|
|
394
|
+
const currentPid = params.currentPid ?? process.pid;
|
|
395
|
+
const orphans = listOrphanedOpencodeProcesses(currentPid);
|
|
396
|
+
const terminated: number[] = [];
|
|
397
|
+
|
|
398
|
+
for (const orphan of orphans) {
|
|
399
|
+
try {
|
|
400
|
+
params.logger?.(
|
|
401
|
+
`orphan_opencode_reap_attempt: pid=${orphan.pid}${orphan.name ? ` name=${orphan.name}` : ""} parentPid=${orphan.parentPid} command=${orphan.commandLine}`,
|
|
402
|
+
);
|
|
403
|
+
killProcessTreeSync(orphan.pid);
|
|
404
|
+
if (await waitForProcessExit(orphan.pid, PEER_BRIDGE_EXIT_TIMEOUT_MS)) {
|
|
405
|
+
terminated.push(orphan.pid);
|
|
406
|
+
params.logger?.(`orphan_opencode_reaped: pid=${orphan.pid}`);
|
|
407
|
+
} else {
|
|
408
|
+
params.logger?.(`orphan_opencode_reap_timeout: pid=${orphan.pid}`);
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
params.logger?.(`orphan_opencode_reap_failed: pid=${orphan.pid} error=${message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return terminated;
|
|
417
|
+
}
|