cli-wechat-bridge 1.1.2 → 1.1.4
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/README.md +129 -150
- package/bin/_run-entry.mjs +12 -0
- package/bin/wechat-claude-start.mjs +7 -2
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex-start.mjs +7 -2
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode-start.mjs +7 -2
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi-start.mjs +10 -0
- package/bin/wechat-pi.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +2 -0
- package/dist/bridge/bridge-adapters.claude.js +21 -1
- package/dist/bridge/bridge-adapters.codex.js +85 -6
- package/dist/bridge/bridge-adapters.core.js +112 -6
- package/dist/bridge/bridge-adapters.js +5 -3
- package/dist/bridge/bridge-adapters.opencode.js +390 -52
- package/dist/bridge/bridge-adapters.pi.js +707 -0
- package/dist/bridge/bridge-adapters.shared.js +22 -102
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-state.js +42 -6
- package/dist/bridge/bridge-utils.js +94 -237
- package/dist/bridge/wechat-bridge.js +77 -131
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +20 -6
- package/dist/companion/local-companion-link.js +13 -3
- package/dist/companion/local-companion-start.js +64 -16
- package/dist/companion/local-companion.js +8 -5
- package/dist/companion/pi-tui-bridge-extension.js +256 -0
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +264 -77
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +4 -5
- package/dist/i18n/messages-zh.js +4 -5
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +27 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -9
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +113 -102
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
|
@@ -32,21 +32,25 @@ export const LOCAL_COMPANION_RECONNECT_GRACE_MS = 15_000;
|
|
|
32
32
|
export const CLAUDE_HOOK_LISTEN_HOST = "127.0.0.1";
|
|
33
33
|
export const CLAUDE_HELP_PROBE_TIMEOUT_MS = 5_000;
|
|
34
34
|
export const CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
|
|
35
|
-
export const DEFAULT_UNIX_SHELL_CANDIDATES = ["pwsh", "bash", "zsh", "sh"];
|
|
36
|
-
export const POSIX_SHELL_NAMES = new Set(["bash", "zsh", "sh", "dash", "ksh"]);
|
|
37
35
|
export const CLAUDE_FLAG_SUPPORT_CACHE = new Map();
|
|
38
36
|
export const OPENCODE_SERVER_HOST = "127.0.0.1";
|
|
39
37
|
export const OPENCODE_SERVER_READY_TIMEOUT_MS = 10_000;
|
|
40
38
|
export const OPENCODE_SSE_RECONNECT_DELAY_MS = 2_000;
|
|
41
39
|
export const OPENCODE_SESSION_IDLE_SETTLE_MS = 1_500;
|
|
42
40
|
export const OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
|
|
41
|
+
// `opencode serve` binds its TCP port before the HTTP layer is ready. The
|
|
42
|
+
// health probe therefore retries within this total budget so the startup
|
|
43
|
+
// race self-heals instead of hanging until undici's ~300s headersTimeout.
|
|
44
|
+
export const OPENCODE_HTTP_READY_TIMEOUT_MS = 15_000;
|
|
45
|
+
export const OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS = 3_000;
|
|
46
|
+
export const OPENCODE_HTTP_READY_PROBE_INTERVAL_MS = 500;
|
|
43
47
|
export function buildCodexCliArgs(remoteUrl, options = {}) {
|
|
44
48
|
assertNoReservedExtraCliArgs(options.extraCliArgs ?? [], ["--remote", "--remote-auth-token-env"], "Codex remote connection");
|
|
45
49
|
const args = [];
|
|
46
50
|
if (options.resumeThreadId) {
|
|
47
51
|
args.push("resume", options.resumeThreadId);
|
|
48
52
|
}
|
|
49
|
-
args.push("--
|
|
53
|
+
args.push("--remote", remoteUrl);
|
|
50
54
|
if (options.inlineMode) {
|
|
51
55
|
args.push("--no-alt-screen");
|
|
52
56
|
}
|
|
@@ -672,44 +676,25 @@ function applyLoopbackNoProxy(env) {
|
|
|
672
676
|
env.no_proxy = mergeNoProxyValue(env.no_proxy);
|
|
673
677
|
return env;
|
|
674
678
|
}
|
|
675
|
-
export function resolveDefaultAdapterCommand(kind,
|
|
676
|
-
|
|
677
|
-
if (kind !== "shell") {
|
|
678
|
-
return kind;
|
|
679
|
-
}
|
|
680
|
-
if (platform === "win32") {
|
|
681
|
-
return "powershell.exe";
|
|
682
|
-
}
|
|
683
|
-
const env = options.env ?? process.env;
|
|
684
|
-
for (const candidate of DEFAULT_UNIX_SHELL_CANDIDATES) {
|
|
685
|
-
if (resolveCommandPath(candidate, platform, env)) {
|
|
686
|
-
return candidate;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
throw new Error(`No default shell executable was found on ${platform}. Tried: ${DEFAULT_UNIX_SHELL_CANDIDATES.join(", ")}. Use --cmd <executable>.`);
|
|
679
|
+
export function resolveDefaultAdapterCommand(kind, _options = {}) {
|
|
680
|
+
return kind;
|
|
690
681
|
}
|
|
691
|
-
export function buildCliEnvironment(
|
|
682
|
+
export function buildCliEnvironment(_kind, options = {}) {
|
|
692
683
|
const sourceEnv = options.env ?? process.env;
|
|
693
684
|
const platform = options.platform ?? process.platform;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const env = {
|
|
701
|
-
...copyDefinedEnv(sourceEnv),
|
|
702
|
-
TERM: sourceEnv.TERM || "xterm-256color",
|
|
703
|
-
};
|
|
704
|
-
if (platform === "win32" && !env.HOME && env.USERPROFILE) {
|
|
705
|
-
env.HOME = env.USERPROFILE;
|
|
706
|
-
}
|
|
707
|
-
return applyLoopbackNoProxy(env);
|
|
708
|
-
}
|
|
709
|
-
return {
|
|
685
|
+
// Pass the full environment through on every platform. A previous
|
|
686
|
+
// Windows-only allowlist silently dropped user-configured variables such
|
|
687
|
+
// as ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / OPENAI_API_KEY, which
|
|
688
|
+
// broke CLIs that authenticate via environment variables even though the
|
|
689
|
+
// same CLI worked when launched manually from the user's shell.
|
|
690
|
+
const env = {
|
|
710
691
|
...copyDefinedEnv(sourceEnv),
|
|
711
692
|
TERM: sourceEnv.TERM || "xterm-256color",
|
|
712
693
|
};
|
|
694
|
+
if (platform === "win32" && !env.HOME && env.USERPROFILE) {
|
|
695
|
+
env.HOME = env.USERPROFILE;
|
|
696
|
+
}
|
|
697
|
+
return applyLoopbackNoProxy(env);
|
|
713
698
|
}
|
|
714
699
|
// ConPTY shipped in Windows 10 build 18309; on older builds node-pty must be
|
|
715
700
|
// left to auto-select its winpty fallback instead of being forced onto ConPTY.
|
|
@@ -732,72 +717,6 @@ export function buildPtySpawnOptions(params) {
|
|
|
732
717
|
}
|
|
733
718
|
return options;
|
|
734
719
|
}
|
|
735
|
-
export function normalizeShellCommandName(command) {
|
|
736
|
-
return path.parse(path.basename(command)).name.toLowerCase();
|
|
737
|
-
}
|
|
738
|
-
export function resolveShellRuntime(command, options = {}) {
|
|
739
|
-
const platform = options.platform ?? process.platform;
|
|
740
|
-
const name = normalizeShellCommandName(command);
|
|
741
|
-
if (name === "powershell" || name === "pwsh") {
|
|
742
|
-
return {
|
|
743
|
-
family: "powershell",
|
|
744
|
-
launchArgs: platform === "win32"
|
|
745
|
-
? ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-NoExit"]
|
|
746
|
-
: ["-NoLogo", "-NoProfile", "-NoExit"],
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
if (POSIX_SHELL_NAMES.has(name)) {
|
|
750
|
-
return {
|
|
751
|
-
family: "posix",
|
|
752
|
-
launchArgs: ["-i"],
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
throw new Error(`Unsupported shell executable for shell adapter: ${command}. Supported shells: powershell, pwsh, bash, zsh, sh, dash, ksh.`);
|
|
756
|
-
}
|
|
757
|
-
export function escapePowerShellString(text) {
|
|
758
|
-
return text.replace(/`/g, "``").replace(/"/g, '`"');
|
|
759
|
-
}
|
|
760
|
-
export function escapePosixShellString(text) {
|
|
761
|
-
return `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
762
|
-
}
|
|
763
|
-
export function buildShellProfileCommand(profilePath, family) {
|
|
764
|
-
const resolved = path.resolve(profilePath);
|
|
765
|
-
if (family === "powershell") {
|
|
766
|
-
return `. "${escapePowerShellString(resolved)}"`;
|
|
767
|
-
}
|
|
768
|
-
return `. ${escapePosixShellString(resolved)}`;
|
|
769
|
-
}
|
|
770
|
-
export function buildShellInputPayload(text, family, completionMarker = "__WECHAT_BRIDGE_DONE__") {
|
|
771
|
-
if (family === "powershell") {
|
|
772
|
-
const encodedCommand = Buffer.from(text, "utf8").toString("base64");
|
|
773
|
-
const script = [
|
|
774
|
-
"$__wechatBridgePreviousErrorActionPreference = $ErrorActionPreference",
|
|
775
|
-
"$ErrorActionPreference = 'Continue'",
|
|
776
|
-
"$global:LASTEXITCODE = 0",
|
|
777
|
-
"try {",
|
|
778
|
-
` $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${escapePowerShellString(encodedCommand)}"))`,
|
|
779
|
-
" $scriptBlock = [scriptblock]::Create($decoded)",
|
|
780
|
-
" & $scriptBlock",
|
|
781
|
-
"} catch {",
|
|
782
|
-
" Write-Error $_",
|
|
783
|
-
" $global:LASTEXITCODE = 1",
|
|
784
|
-
"} finally {",
|
|
785
|
-
" if (-not ($global:LASTEXITCODE -is [int])) { $global:LASTEXITCODE = 0 }",
|
|
786
|
-
` Write-Output "${escapePowerShellString(completionMarker)}:$global:LASTEXITCODE"`,
|
|
787
|
-
" $ErrorActionPreference = $__wechatBridgePreviousErrorActionPreference",
|
|
788
|
-
"}",
|
|
789
|
-
"",
|
|
790
|
-
];
|
|
791
|
-
return `${script.join("\r")}\r`;
|
|
792
|
-
}
|
|
793
|
-
const script = [
|
|
794
|
-
text,
|
|
795
|
-
"__wechat_bridge_status=$?",
|
|
796
|
-
`printf '%s:%s\\n' ${escapePosixShellString(completionMarker)} "$__wechat_bridge_status"`,
|
|
797
|
-
"",
|
|
798
|
-
];
|
|
799
|
-
return `${script.join("\r")}\r`;
|
|
800
|
-
}
|
|
801
720
|
export async function reserveLocalPort() {
|
|
802
721
|
return await new Promise((resolve, reject) => {
|
|
803
722
|
const server = net.createServer();
|
|
@@ -1170,7 +1089,8 @@ export function resolveSpawnTarget(command, kind, options = {}) {
|
|
|
1170
1089
|
return { file: trimmed, args: [...forwardArgs] };
|
|
1171
1090
|
}
|
|
1172
1091
|
const resolved = resolveCommandPath(trimmed, platform, env) ?? trimmed;
|
|
1173
|
-
if (platform !== "win32" ||
|
|
1092
|
+
if (platform !== "win32" ||
|
|
1093
|
+
(kind !== "codex" && kind !== "claude" && kind !== "opencode" && kind !== "pi")) {
|
|
1174
1094
|
return { file: resolved, args: [...forwardArgs] };
|
|
1175
1095
|
}
|
|
1176
1096
|
const bundledExe = kind === "codex" || kind === "claude"
|
|
@@ -8,9 +8,24 @@ export async function forwardWechatFinalReply(params) {
|
|
|
8
8
|
if (visibleText) {
|
|
9
9
|
// Send long replies in bounded chunks: a single oversized sendmessage call
|
|
10
10
|
// can be rejected by the WeChat API, silently losing the whole reply.
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const chunks = splitWechatTextIntoChunks(visibleText);
|
|
12
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
13
|
+
const sent = await sender.sendText(chunks[index]);
|
|
13
14
|
if (sent === false) {
|
|
15
|
+
// The send channel is failing (e.g. an expired context token).
|
|
16
|
+
// Report what was dropped instead of ending mid-reply silently;
|
|
17
|
+
// this notice itself is best effort and may also fail.
|
|
18
|
+
const remainingChunks = chunks.length - index - 1;
|
|
19
|
+
if (remainingChunks > 0 || parsed.attachments.length > 0) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (remainingChunks > 0) {
|
|
22
|
+
parts.push(`${remainingChunks} reply chunk(s)`);
|
|
23
|
+
}
|
|
24
|
+
if (parsed.attachments.length > 0) {
|
|
25
|
+
parts.push(`${parsed.attachments.length} attachment(s)`);
|
|
26
|
+
}
|
|
27
|
+
await sender.sendText(`[bridge] Reply delivery was interrupted; ${parts.join(" and ")} could not be sent. Send a new message to retry.`);
|
|
28
|
+
}
|
|
14
29
|
return;
|
|
15
30
|
}
|
|
16
31
|
}
|
|
@@ -56,9 +56,14 @@ export function isWechatDaemonCommandLineForCwd(commandLine, cwd) {
|
|
|
56
56
|
* opencode serve --port 12345 --hostname 127.0.0.1
|
|
57
57
|
* opencode.exe serve --port 12345
|
|
58
58
|
* Also matches wrapper invocations (opencode.cmd / opencode.bat) on Windows.
|
|
59
|
+
*
|
|
60
|
+
* `serve` must be the first subcommand token after the executable so that
|
|
61
|
+
* unrelated command lines merely containing both words (e.g.
|
|
62
|
+
* `node opencode-lint/serve.js` or `mytool --theme opencode --mode serve`)
|
|
63
|
+
* are not reaped as orphans.
|
|
59
64
|
*/
|
|
60
65
|
export function isOpencodeServeCommandLine(commandLine) {
|
|
61
|
-
return /\bopencode(?:\.exe|\.cmd|\.bat)?\
|
|
66
|
+
return /\bopencode(?:\.exe|\.cmd|\.bat)?["']?\s+serve\b/i.test(commandLine);
|
|
62
67
|
}
|
|
63
68
|
/**
|
|
64
69
|
* Detects `opencode attach` command lines spawned by the local OpenCode companion.
|
|
@@ -67,7 +72,7 @@ export function isOpencodeServeCommandLine(commandLine) {
|
|
|
67
72
|
* opencode.exe attach http://127.0.0.1:12345 --session ses_123
|
|
68
73
|
*/
|
|
69
74
|
export function isOpencodeAttachCommandLine(commandLine) {
|
|
70
|
-
return /\bopencode(?:\.exe|\.cmd|\.bat)?\
|
|
75
|
+
return /\bopencode(?:\.exe|\.cmd|\.bat)?["']?\s+attach\b/i.test(commandLine);
|
|
71
76
|
}
|
|
72
77
|
function normalizeBridgeProcessRecord(value) {
|
|
73
78
|
if (!isRecord(value)) {
|
|
@@ -158,7 +163,8 @@ function listWindowsBridgeProcesses(currentPid = process.pid) {
|
|
|
158
163
|
"Bypass",
|
|
159
164
|
"-Command",
|
|
160
165
|
[
|
|
161
|
-
|
|
166
|
+
// Trailing semicolon required — see the note in listAllProcessesRaw.
|
|
167
|
+
"$ErrorActionPreference='Stop';",
|
|
162
168
|
"Get-CimInstance Win32_Process",
|
|
163
169
|
"| Where-Object { $_.CommandLine }",
|
|
164
170
|
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
@@ -236,7 +242,11 @@ function listAllProcessesRaw(currentPid = process.pid) {
|
|
|
236
242
|
"Bypass",
|
|
237
243
|
"-Command",
|
|
238
244
|
[
|
|
239
|
-
|
|
245
|
+
// Statement separator required: without the trailing semicolon
|
|
246
|
+
// PowerShell parses the preference assignment and the first command
|
|
247
|
+
// as one statement and fails with a ParserError, silently
|
|
248
|
+
// returning an empty process list on every probe.
|
|
249
|
+
"$ErrorActionPreference='Stop';",
|
|
240
250
|
"Get-CimInstance Win32_Process",
|
|
241
251
|
"| Where-Object { $_.CommandLine }",
|
|
242
252
|
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureWorkspaceChannelDir, ensureChannelDataDir, } from "../wechat/channel-config.js";
|
|
3
3
|
import { buildInstanceId } from "./bridge-utils.js";
|
|
4
|
+
import { writeJsonFileAtomic } from "../utils/atomic-file.js";
|
|
5
|
+
import { getProcessRecordByPid, isWechatBridgeCommandLine, } from "./bridge-process-reaper.js";
|
|
4
6
|
const ORPHAN_LOCK_RECLAIM_TIMEOUT_MS = 2_000;
|
|
5
7
|
const ORPHAN_LOCK_RECLAIM_POLL_MS = 100;
|
|
6
8
|
export function resolveRestorableSharedSessionId(persisted, options) {
|
|
@@ -59,6 +61,7 @@ export function normalizeBridgeLockPayload(value) {
|
|
|
59
61
|
const adapter = record.adapter === "codex" ||
|
|
60
62
|
record.adapter === "claude" ||
|
|
61
63
|
record.adapter === "opencode" ||
|
|
64
|
+
record.adapter === "pi" ||
|
|
62
65
|
record.adapter === "shell"
|
|
63
66
|
? record.adapter
|
|
64
67
|
: null;
|
|
@@ -136,7 +139,37 @@ export function evaluateBridgeRuntimeOwnership(params) {
|
|
|
136
139
|
function buildLockConflictError(lock) {
|
|
137
140
|
return new Error(`Another bridge instance is already running (pid=${lock.pid}, instanceId=${lock.instanceId}, adapter=${lock.adapter}, cwd=${lock.cwd}, startedAt=${lock.startedAt}, lifecycle=${lock.lifecycle}). Stop it before starting a new bridge.`);
|
|
138
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Check whether the process currently holding a stale lock is still a
|
|
144
|
+
* wechat-bridge process. OS pid reuse can hand the recorded pid to an
|
|
145
|
+
* unrelated process; terminating that pid would kill an innocent process.
|
|
146
|
+
*
|
|
147
|
+
* Returns "bridge" when the command line matches a bridge, "foreign" when the
|
|
148
|
+
* pid is alive but belongs to something else, and "unknown" when the command
|
|
149
|
+
* line could not be resolved (probe failure, restricted permissions).
|
|
150
|
+
*/
|
|
151
|
+
export function classifyLockHolderProcess(pid) {
|
|
152
|
+
if (!isPidAlive(pid)) {
|
|
153
|
+
return "unknown";
|
|
154
|
+
}
|
|
155
|
+
const record = getProcessRecordByPid(pid);
|
|
156
|
+
if (!record) {
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
return isWechatBridgeCommandLine(record.commandLine) ? "bridge" : "foreign";
|
|
160
|
+
}
|
|
139
161
|
function tryTerminateOrphanedBridge(lock) {
|
|
162
|
+
const holderKind = classifyLockHolderProcess(lock.pid);
|
|
163
|
+
if (holderKind === "foreign") {
|
|
164
|
+
// The lock-holding bridge is gone and its pid was reused by an unrelated
|
|
165
|
+
// process. Reclaim the lock without killing that process.
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (holderKind === "unknown") {
|
|
169
|
+
// Could not confirm the pid still belongs to a bridge. Do not risk
|
|
170
|
+
// killing an innocent process; keep the conflict error instead.
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
140
173
|
try {
|
|
141
174
|
process.kill(lock.pid);
|
|
142
175
|
}
|
|
@@ -294,13 +327,9 @@ export class BridgeStateStore {
|
|
|
294
327
|
}
|
|
295
328
|
}
|
|
296
329
|
writeAtomicJsonFile(filePath, value) {
|
|
297
|
-
ensureChannelDataDir();
|
|
298
|
-
const data = JSON.stringify(value, null, 2);
|
|
299
330
|
// temp + rename so a crash mid-write cannot leave a truncated/half-written
|
|
300
331
|
// state or lock file that would break the next launch or look like a stale lock.
|
|
301
|
-
|
|
302
|
-
fs.writeFileSync(tempPath, data, "utf-8");
|
|
303
|
-
fs.renameSync(tempPath, filePath);
|
|
332
|
+
writeJsonFileAtomic(filePath, value);
|
|
304
333
|
}
|
|
305
334
|
save() {
|
|
306
335
|
this.writeAtomicJsonFile(this.stateFilePath, this.state);
|
|
@@ -310,7 +339,14 @@ export class BridgeStateStore {
|
|
|
310
339
|
if (existing &&
|
|
311
340
|
existing.pid !== process.pid &&
|
|
312
341
|
isPidAlive(existing.pid)) {
|
|
313
|
-
|
|
342
|
+
const holderKind = classifyLockHolderProcess(existing.pid);
|
|
343
|
+
if (holderKind === "foreign") {
|
|
344
|
+
// The recorded bridge exited and the OS reused its pid for an
|
|
345
|
+
// unrelated process. The lock is stale: reclaim it instead of
|
|
346
|
+
// dead-locking new bridges on a phantom holder.
|
|
347
|
+
this.appendLog(`stale_lock_pid_reuse: pid=${existing.pid} instanceId=${existing.instanceId} adapter=${existing.adapter} cwd=${existing.cwd}`);
|
|
348
|
+
}
|
|
349
|
+
else if (shouldAutoReclaimBridgeLock(existing)) {
|
|
314
350
|
this.appendLog(`lock_reclaim_attempt: pid=${existing.pid} instanceId=${existing.instanceId} adapter=${existing.adapter} cwd=${existing.cwd}`);
|
|
315
351
|
if (tryTerminateOrphanedBridge(existing)) {
|
|
316
352
|
this.appendLog(`lock_reclaimed: pid=${existing.pid} instanceId=${existing.instanceId} adapter=${existing.adapter} cwd=${existing.cwd}`);
|