codex-to-im 1.0.41 → 1.0.42

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/dist/cli.mjs CHANGED
@@ -5,9 +5,9 @@ import { createRequire } from 'module'; const require = createRequire(import.met
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
 
7
7
  // src/service-manager.ts
8
- import fs2 from "node:fs";
8
+ import fs3 from "node:fs";
9
9
  import os2 from "node:os";
10
- import path2 from "node:path";
10
+ import path3 from "node:path";
11
11
  import { spawn } from "node:child_process";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
@@ -233,42 +233,79 @@ function loadConfig() {
233
233
  return expandConfig(empty);
234
234
  }
235
235
 
236
- // src/service-manager.ts
237
- var moduleDir = path2.dirname(fileURLToPath(import.meta.url));
238
- var packageRoot = path2.resolve(moduleDir, "..");
236
+ // src/bridge-instance-lock.ts
237
+ import fs2 from "node:fs";
238
+ import path2 from "node:path";
239
239
  var runtimeDir = path2.join(CTI_HOME, "runtime");
240
- var logsDir = path2.join(CTI_HOME, "logs");
241
- var bridgePidFile = path2.join(runtimeDir, "bridge.pid");
242
- var bridgeStatusFile = path2.join(runtimeDir, "status.json");
243
- var bridgeStartLockFile = path2.join(runtimeDir, "bridge.start.lock");
244
- var uiStatusFile = path2.join(runtimeDir, "ui-server.json");
240
+ var bridgeInstanceLockFile = path2.join(runtimeDir, "bridge.instance.lock");
241
+ function readJsonFile(filePath, fallback) {
242
+ try {
243
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
244
+ } catch {
245
+ return fallback;
246
+ }
247
+ }
248
+ function isProcessAlive(pid) {
249
+ if (!pid) return false;
250
+ try {
251
+ process.kill(pid, 0);
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+ function readBridgeInstanceLock(filePath = bridgeInstanceLockFile) {
258
+ const parsed = readJsonFile(filePath, null);
259
+ const pid = Number(parsed?.pid);
260
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
261
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
262
+ return { pid, createdAt };
263
+ }
264
+ function clearStaleBridgeInstanceLock(filePath = bridgeInstanceLockFile, isAlive = isProcessAlive) {
265
+ const existing = readBridgeInstanceLock(filePath);
266
+ if (existing && isAlive(existing.pid)) return;
267
+ try {
268
+ fs2.unlinkSync(filePath);
269
+ } catch {
270
+ }
271
+ }
272
+
273
+ // src/service-manager.ts
274
+ var moduleDir = path3.dirname(fileURLToPath(import.meta.url));
275
+ var packageRoot = path3.resolve(moduleDir, "..");
276
+ var runtimeDir2 = path3.join(CTI_HOME, "runtime");
277
+ var logsDir = path3.join(CTI_HOME, "logs");
278
+ var bridgePidFile = path3.join(runtimeDir2, "bridge.pid");
279
+ var bridgeStatusFile = path3.join(runtimeDir2, "status.json");
280
+ var bridgeStartLockFile = path3.join(runtimeDir2, "bridge.start.lock");
281
+ var uiStatusFile = path3.join(runtimeDir2, "ui-server.json");
245
282
  var uiPort = 4781;
246
283
  var bridgeAutostartTaskName = "CodexToIMBridge";
247
- var bridgeAutostartLauncherFile = path2.join(runtimeDir, "bridge-autostart.ps1");
248
- var npmUninstallLogFile = path2.join(runtimeDir, "npm-uninstall.log");
284
+ var bridgeAutostartLauncherFile = path3.join(runtimeDir2, "bridge-autostart.ps1");
285
+ var npmUninstallLogFile = path3.join(runtimeDir2, "npm-uninstall.log");
249
286
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
250
287
  var BRIDGE_START_LOCK_STALE_MS = 3e4;
251
288
  function ensureDirs() {
252
- fs2.mkdirSync(runtimeDir, { recursive: true });
253
- fs2.mkdirSync(logsDir, { recursive: true });
289
+ fs3.mkdirSync(runtimeDir2, { recursive: true });
290
+ fs3.mkdirSync(logsDir, { recursive: true });
254
291
  }
255
- function readJsonFile(filePath, fallback) {
292
+ function readJsonFile2(filePath, fallback) {
256
293
  try {
257
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
294
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
258
295
  } catch {
259
296
  return fallback;
260
297
  }
261
298
  }
262
299
  function readPid(filePath) {
263
300
  try {
264
- const raw = fs2.readFileSync(filePath, "utf-8").trim();
301
+ const raw = fs3.readFileSync(filePath, "utf-8").trim();
265
302
  const pid = Number(raw);
266
303
  return Number.isFinite(pid) ? pid : void 0;
267
304
  } catch {
268
305
  return void 0;
269
306
  }
270
307
  }
271
- function isProcessAlive(pid) {
308
+ function isProcessAlive2(pid) {
272
309
  if (!pid) return false;
273
310
  try {
274
311
  process.kill(pid, 0);
@@ -277,32 +314,37 @@ function isProcessAlive(pid) {
277
314
  return false;
278
315
  }
279
316
  }
280
- function collectTrackedBridgePids(bridgePid, statusPid) {
317
+ function collectTrackedBridgePids(bridgePid, statusPid, instanceLockPid) {
281
318
  const unique = /* @__PURE__ */ new Set();
282
- for (const pid of [bridgePid, statusPid]) {
319
+ for (const pid of [bridgePid, statusPid, instanceLockPid]) {
283
320
  if (Number.isFinite(pid) && pid > 0) {
284
321
  unique.add(pid);
285
322
  }
286
323
  }
287
324
  return [...unique];
288
325
  }
289
- function resolveTrackedBridgePid(bridgePid, statusPid, isAlive = isProcessAlive) {
326
+ function resolveTrackedBridgePid(bridgePid, statusPid, instanceLockPid, isAlive = isProcessAlive2) {
290
327
  if (isAlive(bridgePid)) return bridgePid;
291
328
  if (isAlive(statusPid)) return statusPid;
292
- return bridgePid ?? statusPid;
329
+ if (isAlive(instanceLockPid)) return instanceLockPid;
330
+ return bridgePid ?? statusPid ?? instanceLockPid;
293
331
  }
294
332
  function getTrackedBridgePids(status) {
295
- const resolvedStatus = status ?? readJsonFile(bridgeStatusFile, { running: false });
296
- return collectTrackedBridgePids(readPid(bridgePidFile), resolvedStatus.pid);
333
+ const resolvedStatus = status ?? readJsonFile2(bridgeStatusFile, { running: false });
334
+ return collectTrackedBridgePids(
335
+ readPid(bridgePidFile),
336
+ resolvedStatus.pid,
337
+ readBridgeInstanceLock()?.pid
338
+ );
297
339
  }
298
340
  function clearBridgePidFile() {
299
341
  try {
300
- fs2.unlinkSync(bridgePidFile);
342
+ fs3.unlinkSync(bridgePidFile);
301
343
  } catch {
302
344
  }
303
345
  }
304
346
  function readBridgeStartLock(filePath = bridgeStartLockFile) {
305
- const parsed = readJsonFile(filePath, null);
347
+ const parsed = readJsonFile2(filePath, null);
306
348
  const pid = Number(parsed?.pid);
307
349
  const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
308
350
  if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
@@ -312,7 +354,7 @@ function isBridgeStartLockStale(lock, options = {}) {
312
354
  if (!lock) return true;
313
355
  const nowMs = options.nowMs ?? Date.now();
314
356
  const staleMs = options.staleMs ?? BRIDGE_START_LOCK_STALE_MS;
315
- const isAlive = options.isAlive ?? isProcessAlive;
357
+ const isAlive = options.isAlive ?? isProcessAlive2;
316
358
  const createdAtMs = Date.parse(lock.createdAt);
317
359
  if (!Number.isFinite(createdAtMs)) return true;
318
360
  if (!isAlive(lock.pid)) return true;
@@ -328,7 +370,7 @@ function tryAcquireBridgeStartLock(options = {}) {
328
370
  };
329
371
  for (let attempt = 0; attempt < 2; attempt += 1) {
330
372
  try {
331
- fs2.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
373
+ fs3.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
332
374
  return { acquired: true };
333
375
  } catch (error) {
334
376
  const code = error.code;
@@ -342,7 +384,7 @@ function tryAcquireBridgeStartLock(options = {}) {
342
384
  return { acquired: false, holderPid: existing2?.pid };
343
385
  }
344
386
  try {
345
- fs2.unlinkSync(filePath);
387
+ fs3.unlinkSync(filePath);
346
388
  } catch {
347
389
  }
348
390
  }
@@ -354,14 +396,14 @@ function releaseBridgeStartLock(filePath = bridgeStartLockFile, ownerPid = proce
354
396
  const existing = readBridgeStartLock(filePath);
355
397
  if (!existing) {
356
398
  try {
357
- fs2.unlinkSync(filePath);
399
+ fs3.unlinkSync(filePath);
358
400
  } catch {
359
401
  }
360
402
  return;
361
403
  }
362
404
  if (existing.pid !== ownerPid) return;
363
405
  try {
364
- fs2.unlinkSync(filePath);
406
+ fs3.unlinkSync(filePath);
365
407
  } catch {
366
408
  }
367
409
  }
@@ -443,11 +485,11 @@ function ensureBridgeAutostartLauncher() {
443
485
  " }",
444
486
  " } catch { }",
445
487
  "}",
446
- `& $node '${escapePowerShellSingleQuoted(path2.join(packageRoot, "dist", "cli.mjs"))}' start`,
488
+ `& $node '${escapePowerShellSingleQuoted(path3.join(packageRoot, "dist", "cli.mjs"))}' start`,
447
489
  "exit $LASTEXITCODE",
448
490
  ""
449
491
  ].join("\r\n");
450
- fs2.writeFileSync(bridgeAutostartLauncherFile, content, "utf-8");
492
+ fs3.writeFileSync(bridgeAutostartLauncherFile, content, "utf-8");
451
493
  return bridgeAutostartLauncherFile;
452
494
  }
453
495
  function parsePowerShellJson(raw) {
@@ -491,7 +533,7 @@ function buildDeferredGlobalNpmUninstallLaunch(options = {}) {
491
533
  async function launchDeferredGlobalNpmUninstall() {
492
534
  ensureDirs();
493
535
  const launch = buildDeferredGlobalNpmUninstallLaunch();
494
- fs2.writeFileSync(
536
+ fs3.writeFileSync(
495
537
  launch.logPath,
496
538
  [
497
539
  `[${(/* @__PURE__ */ new Date()).toISOString()}] Scheduling global uninstall.`,
@@ -519,14 +561,18 @@ function getUiServerUrl(port = uiPort) {
519
561
  return `http://127.0.0.1:${port}`;
520
562
  }
521
563
  function getCurrentUiServerUrl() {
522
- const status = readJsonFile(uiStatusFile, null);
564
+ const status = readJsonFile2(uiStatusFile, null);
523
565
  if (!status?.port) return void 0;
524
566
  return getUiServerUrl(status.port);
525
567
  }
526
568
  function getBridgeStatus() {
527
- const status = readJsonFile(bridgeStatusFile, { running: false });
528
- const pid = resolveTrackedBridgePid(readPid(bridgePidFile), status.pid);
529
- if (!isProcessAlive(pid)) {
569
+ const status = readJsonFile2(bridgeStatusFile, { running: false });
570
+ const pid = resolveTrackedBridgePid(
571
+ readPid(bridgePidFile),
572
+ status.pid,
573
+ readBridgeInstanceLock()?.pid
574
+ );
575
+ if (!isProcessAlive2(pid)) {
530
576
  return {
531
577
  ...status,
532
578
  pid,
@@ -540,8 +586,8 @@ function getBridgeStatus() {
540
586
  };
541
587
  }
542
588
  function getUiServerStatus() {
543
- const status = readJsonFile(uiStatusFile, { running: false, port: uiPort });
544
- if (!isProcessAlive(status.pid)) {
589
+ const status = readJsonFile2(uiStatusFile, { running: false, port: uiPort });
590
+ if (!isProcessAlive2(status.pid)) {
545
591
  return {
546
592
  ...status,
547
593
  running: false,
@@ -626,7 +672,7 @@ async function waitForUiServer(timeoutMs = 15e3) {
626
672
  async function startBridge() {
627
673
  ensureDirs();
628
674
  const current = getBridgeStatus();
629
- const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive(pid));
675
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive2(pid));
630
676
  if (current.running && extraAlivePids.length === 0) return current;
631
677
  if (current.running && extraAlivePids.length > 0) {
632
678
  await stopBridge();
@@ -651,17 +697,17 @@ async function startBridge() {
651
697
  startLockHeld = true;
652
698
  try {
653
699
  const currentAfterLock = getBridgeStatus();
654
- const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive(pid));
700
+ const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive2(pid));
655
701
  if (currentAfterLock.running && extraAlivePidsAfterLock.length === 0) return currentAfterLock;
656
702
  if (currentAfterLock.running && extraAlivePidsAfterLock.length > 0) {
657
703
  await stopBridge();
658
704
  }
659
- const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
660
- if (!fs2.existsSync(daemonEntry)) {
705
+ const daemonEntry = path3.join(packageRoot, "dist", "daemon.mjs");
706
+ if (!fs3.existsSync(daemonEntry)) {
661
707
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
662
708
  }
663
- const stdoutFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.out.log"), "a");
664
- const stderrFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.err.log"), "a");
709
+ const stdoutFd = fs3.openSync(path3.join(logsDir, "bridge-launcher.out.log"), "a");
710
+ const stderrFd = fs3.openSync(path3.join(logsDir, "bridge-launcher.err.log"), "a");
665
711
  const child = spawn(process.execPath, [daemonEntry], {
666
712
  cwd: packageRoot,
667
713
  detached: true,
@@ -684,10 +730,11 @@ async function startBridge() {
684
730
  }
685
731
  }
686
732
  async function stopBridge() {
687
- const status = readJsonFile(bridgeStatusFile, { running: false });
688
- const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive(pid));
733
+ const status = readJsonFile2(bridgeStatusFile, { running: false });
734
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive2(pid));
689
735
  if (pids.length === 0) {
690
736
  clearBridgePidFile();
737
+ clearStaleBridgeInstanceLock();
691
738
  return { ...getBridgeStatus(), running: false };
692
739
  }
693
740
  for (const pid of pids) {
@@ -709,13 +756,15 @@ async function stopBridge() {
709
756
  }
710
757
  const startedAt = Date.now();
711
758
  while (Date.now() - startedAt < 1e4) {
712
- if (pids.every((pid) => !isProcessAlive(pid))) {
759
+ if (pids.every((pid) => !isProcessAlive2(pid))) {
713
760
  clearBridgePidFile();
761
+ clearStaleBridgeInstanceLock();
714
762
  return getBridgeStatus();
715
763
  }
716
764
  await sleep(300);
717
765
  }
718
766
  clearBridgePidFile();
767
+ clearStaleBridgeInstanceLock();
719
768
  return getBridgeStatus();
720
769
  }
721
770
  async function getBridgeAutostartStatus() {
@@ -804,8 +853,8 @@ async function uninstallBridgeAutostart() {
804
853
  ].join("; ");
805
854
  await runPowerShell(script);
806
855
  try {
807
- if (fs2.existsSync(bridgeAutostartLauncherFile)) {
808
- fs2.unlinkSync(bridgeAutostartLauncherFile);
856
+ if (fs3.existsSync(bridgeAutostartLauncherFile)) {
857
+ fs3.unlinkSync(bridgeAutostartLauncherFile);
809
858
  }
810
859
  } catch {
811
860
  }
@@ -836,12 +885,12 @@ async function ensureUiServerRunning() {
836
885
  ensureDirs();
837
886
  const current = getUiServerStatus();
838
887
  if (current.running) return current;
839
- const serverEntry = path2.join(packageRoot, "dist", "ui-server.mjs");
840
- if (!fs2.existsSync(serverEntry)) {
888
+ const serverEntry = path3.join(packageRoot, "dist", "ui-server.mjs");
889
+ if (!fs3.existsSync(serverEntry)) {
841
890
  throw new Error(`UI server bundle not found at ${serverEntry}. Run npm run build first.`);
842
891
  }
843
- const stdoutFd = fs2.openSync(path2.join(logsDir, "ui-server.out.log"), "a");
844
- const stderrFd = fs2.openSync(path2.join(logsDir, "ui-server.err.log"), "a");
892
+ const stdoutFd = fs3.openSync(path3.join(logsDir, "ui-server.out.log"), "a");
893
+ const stderrFd = fs3.openSync(path3.join(logsDir, "ui-server.err.log"), "a");
845
894
  const child = spawn(process.execPath, [serverEntry], {
846
895
  cwd: packageRoot,
847
896
  detached: true,
@@ -861,7 +910,7 @@ async function ensureUiServerRunning() {
861
910
  }
862
911
  async function stopUiServer() {
863
912
  const status = getUiServerStatus();
864
- if (!status.pid || !isProcessAlive(status.pid)) {
913
+ if (!status.pid || !isProcessAlive2(status.pid)) {
865
914
  const next2 = { ...status, running: false };
866
915
  writeUiServerStatus(next2);
867
916
  return next2;
@@ -898,7 +947,7 @@ async function stopUiServer() {
898
947
  }
899
948
  function writeUiServerStatus(status) {
900
949
  ensureDirs();
901
- fs2.writeFileSync(uiStatusFile, JSON.stringify(status, null, 2), "utf-8");
950
+ fs3.writeFileSync(uiStatusFile, JSON.stringify(status, null, 2), "utf-8");
902
951
  }
903
952
  function openBrowser(url) {
904
953
  if (process.platform === "win32") {
package/dist/daemon.mjs CHANGED
@@ -3935,9 +3935,9 @@ var codex_provider_exports = {};
3935
3935
  __export(codex_provider_exports, {
3936
3936
  CodexProvider: () => CodexProvider
3937
3937
  });
3938
- import fs14 from "node:fs";
3938
+ import fs15 from "node:fs";
3939
3939
  import os4 from "node:os";
3940
- import path15 from "node:path";
3940
+ import path16 from "node:path";
3941
3941
  function toApprovalPolicy(permissionMode) {
3942
3942
  switch (permissionMode) {
3943
3943
  case "never":
@@ -3964,7 +3964,7 @@ function normalizeCodexErrorMessage(message) {
3964
3964
  if (!trimmed) return "Codex \u6267\u884C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
3965
3965
  const lower = trimmed.toLowerCase();
3966
3966
  if (lower.includes("timeout waiting for child process to exit") || lower.includes("reconnecting...")) {
3967
- return "Codex \u4F1A\u8BDD\u6062\u590D\u5931\u8D25\uFF0C\u4E0A\u4E00\u8F6E\u6267\u884C\u8FDB\u7A0B\u672A\u6B63\u5E38\u9000\u51FA\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5982\u679C\u8FDE\u7EED\u5931\u8D25\uFF0C\u8BF7\u65B0\u5F00\u7EBF\u7A0B\u6216\u5207\u6362\u5230 `/t 0`\u3002";
3967
+ return "Codex \u4F1A\u8BDD\u6062\u590D\u5931\u8D25\uFF0C\u4E0A\u4E00\u8F6E\u6267\u884C\u8FDB\u7A0B\u672A\u6B63\u5E38\u9000\u51FA\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5982\u679C\u8FDE\u7EED\u5931\u8D25\uFF0C\u8BF7\u65B0\u5F00\u7EBF\u7A0B\u6216\u5207\u6362\u5230 /t 0\u3002";
3968
3968
  }
3969
3969
  return trimmed;
3970
3970
  }
@@ -4028,6 +4028,9 @@ var init_codex_provider = __esm({
4028
4028
  threadIds = /* @__PURE__ */ new Map();
4029
4029
  constructor(_pendingPerms) {
4030
4030
  }
4031
+ clearCachedThreadId(sessionId) {
4032
+ this.threadIds.delete(sessionId);
4033
+ }
4031
4034
  /**
4032
4035
  * Lazily load the Codex SDK. Throws a clear error if the installation is incomplete.
4033
4036
  */
@@ -4081,13 +4084,13 @@ var init_codex_provider = __esm({
4081
4084
  { type: "text", text: params.prompt }
4082
4085
  ];
4083
4086
  for (const file of imageFiles) {
4084
- if (file.filePath && fs14.existsSync(file.filePath)) {
4087
+ if (file.filePath && fs15.existsSync(file.filePath)) {
4085
4088
  parts.push({ type: "local_image", path: file.filePath });
4086
4089
  continue;
4087
4090
  }
4088
4091
  const ext = MIME_EXT[file.type] || ".png";
4089
- const tmpPath = path15.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
4090
- fs14.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
4092
+ const tmpPath = path16.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
4093
+ fs15.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
4091
4094
  tempFiles.push(tmpPath);
4092
4095
  parts.push({ type: "local_image", path: tmpPath });
4093
4096
  }
@@ -4159,12 +4162,14 @@ var init_codex_provider = __esm({
4159
4162
  }
4160
4163
  case "turn.failed": {
4161
4164
  const error = event.error?.message;
4165
+ self.clearCachedThreadId(params.sessionId);
4162
4166
  controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Turn failed")));
4163
4167
  sawTerminalEvent = true;
4164
4168
  break;
4165
4169
  }
4166
4170
  case "error": {
4167
4171
  const error = event.message;
4172
+ self.clearCachedThreadId(params.sessionId);
4168
4173
  controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Thread error")));
4169
4174
  sawTerminalEvent = true;
4170
4175
  break;
@@ -4187,10 +4192,12 @@ var init_codex_provider = __esm({
4187
4192
  const message = err instanceof Error ? err.message : String(err);
4188
4193
  if (savedThreadId && !retryFresh && !sawAnyEvent && shouldRetryFreshThread(message)) {
4189
4194
  console.warn("[codex-provider] Resume failed, retrying with a fresh thread:", message);
4195
+ self.clearCachedThreadId(params.sessionId);
4190
4196
  savedThreadId = void 0;
4191
4197
  retryFresh = true;
4192
4198
  continue;
4193
4199
  }
4200
+ self.clearCachedThreadId(params.sessionId);
4194
4201
  throw err;
4195
4202
  }
4196
4203
  }
@@ -4198,6 +4205,7 @@ var init_codex_provider = __esm({
4198
4205
  } catch (err) {
4199
4206
  const message = err instanceof Error ? err.message : String(err);
4200
4207
  console.error("[codex-provider] Error:", err instanceof Error ? err.stack || err.message : err);
4208
+ self.clearCachedThreadId(params.sessionId);
4201
4209
  try {
4202
4210
  controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
4203
4211
  controller.close();
@@ -4206,7 +4214,7 @@ var init_codex_provider = __esm({
4206
4214
  } finally {
4207
4215
  for (const tmp of tempFiles) {
4208
4216
  try {
4209
- fs14.unlinkSync(tmp);
4217
+ fs15.unlinkSync(tmp);
4210
4218
  } catch {
4211
4219
  }
4212
4220
  }
@@ -4318,6 +4326,7 @@ var init_codex_provider = __esm({
4318
4326
  break;
4319
4327
  }
4320
4328
  case "error": {
4329
+ this.clearCachedThreadId(sessionId);
4321
4330
  controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(item.message || "Codex error")));
4322
4331
  break;
4323
4332
  }
@@ -4339,8 +4348,8 @@ var init_codex_provider = __esm({
4339
4348
  });
4340
4349
 
4341
4350
  // src/main.ts
4342
- import fs15 from "node:fs";
4343
- import path16 from "node:path";
4351
+ import fs16 from "node:fs";
4352
+ import path17 from "node:path";
4344
4353
  import crypto10 from "node:crypto";
4345
4354
 
4346
4355
  // src/lib/bridge/context.ts
@@ -7160,20 +7169,20 @@ ${trimmedResponse}`;
7160
7169
  buffer = Buffer.concat(chunks);
7161
7170
  } catch (streamErr) {
7162
7171
  console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
7163
- const fs16 = await import("fs");
7172
+ const fs17 = await import("fs");
7164
7173
  const os5 = await import("os");
7165
- const path17 = await import("path");
7166
- const tmpPath = path17.join(os5.tmpdir(), `feishu-dl-${crypto2.randomUUID()}`);
7174
+ const path18 = await import("path");
7175
+ const tmpPath = path18.join(os5.tmpdir(), `feishu-dl-${crypto2.randomUUID()}`);
7167
7176
  try {
7168
7177
  await res.writeFile(tmpPath);
7169
- buffer = fs16.readFileSync(tmpPath);
7178
+ buffer = fs17.readFileSync(tmpPath);
7170
7179
  if (buffer.length > MAX_FILE_SIZE) {
7171
7180
  console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
7172
7181
  return null;
7173
7182
  }
7174
7183
  } finally {
7175
7184
  try {
7176
- fs16.unlinkSync(tmpPath);
7185
+ fs17.unlinkSync(tmpPath);
7177
7186
  } catch {
7178
7187
  }
7179
7188
  }
@@ -23205,10 +23214,87 @@ function setupLogger() {
23205
23214
  console.warn = (...args) => write("WARN", args);
23206
23215
  }
23207
23216
 
23217
+ // src/bridge-instance-lock.ts
23218
+ import fs14 from "node:fs";
23219
+ import path15 from "node:path";
23220
+ var runtimeDir = path15.join(CTI_HOME, "runtime");
23221
+ var bridgeInstanceLockFile = path15.join(runtimeDir, "bridge.instance.lock");
23222
+ function readJsonFile(filePath, fallback) {
23223
+ try {
23224
+ return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
23225
+ } catch {
23226
+ return fallback;
23227
+ }
23228
+ }
23229
+ function isProcessAlive(pid) {
23230
+ if (!pid) return false;
23231
+ try {
23232
+ process.kill(pid, 0);
23233
+ return true;
23234
+ } catch {
23235
+ return false;
23236
+ }
23237
+ }
23238
+ function readBridgeInstanceLock(filePath = bridgeInstanceLockFile) {
23239
+ const parsed = readJsonFile(filePath, null);
23240
+ const pid = Number(parsed?.pid);
23241
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
23242
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
23243
+ return { pid, createdAt };
23244
+ }
23245
+ function tryAcquireBridgeInstanceLock(options = {}) {
23246
+ const filePath = options.filePath ?? bridgeInstanceLockFile;
23247
+ const ownerPid = options.ownerPid ?? process.pid;
23248
+ const nowMs = options.nowMs ?? Date.now();
23249
+ const isAlive = options.isAlive ?? isProcessAlive;
23250
+ const payload = {
23251
+ pid: ownerPid,
23252
+ createdAt: new Date(nowMs).toISOString()
23253
+ };
23254
+ for (let attempt = 0; attempt < 2; attempt += 1) {
23255
+ try {
23256
+ fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
23257
+ fs14.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
23258
+ return { acquired: true };
23259
+ } catch (error) {
23260
+ const code2 = error.code;
23261
+ if (code2 !== "EEXIST") throw error;
23262
+ const existing2 = readBridgeInstanceLock(filePath);
23263
+ if (existing2 && existing2.pid !== ownerPid && isAlive(existing2.pid)) {
23264
+ return { acquired: false, holderPid: existing2.pid };
23265
+ }
23266
+ try {
23267
+ fs14.unlinkSync(filePath);
23268
+ } catch {
23269
+ }
23270
+ }
23271
+ }
23272
+ const existing = readBridgeInstanceLock(filePath);
23273
+ if (existing && existing.pid !== ownerPid && isAlive(existing.pid)) {
23274
+ return { acquired: false, holderPid: existing.pid };
23275
+ }
23276
+ return existing?.pid === ownerPid ? { acquired: true } : { acquired: false, holderPid: existing?.pid };
23277
+ }
23278
+ function releaseBridgeInstanceLock(filePath = bridgeInstanceLockFile, ownerPid = process.pid) {
23279
+ const existing = readBridgeInstanceLock(filePath);
23280
+ if (!existing) {
23281
+ try {
23282
+ fs14.unlinkSync(filePath);
23283
+ } catch {
23284
+ }
23285
+ return;
23286
+ }
23287
+ if (existing.pid !== ownerPid) return;
23288
+ try {
23289
+ fs14.unlinkSync(filePath);
23290
+ } catch {
23291
+ }
23292
+ }
23293
+
23208
23294
  // src/main.ts
23209
- var RUNTIME_DIR = path16.join(CTI_HOME, "runtime");
23210
- var STATUS_FILE = path16.join(RUNTIME_DIR, "status.json");
23211
- var PID_FILE = path16.join(RUNTIME_DIR, "bridge.pid");
23295
+ var RUNTIME_DIR = path17.join(CTI_HOME, "runtime");
23296
+ var STATUS_FILE = path17.join(RUNTIME_DIR, "status.json");
23297
+ var PID_FILE = path17.join(RUNTIME_DIR, "bridge.pid");
23212
23298
  async function resolveProvider(config2, pendingPerms) {
23213
23299
  const runtime = config2.runtime;
23214
23300
  if (runtime === "codex") {
@@ -23258,16 +23344,16 @@ async function resolveProvider(config2, pendingPerms) {
23258
23344
  return new SDKLLMProvider(pendingPerms, cliPath, config2.autoApprove);
23259
23345
  }
23260
23346
  function writeStatus(info) {
23261
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
23347
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
23262
23348
  let existing = {};
23263
23349
  try {
23264
- existing = JSON.parse(fs15.readFileSync(STATUS_FILE, "utf-8"));
23350
+ existing = JSON.parse(fs16.readFileSync(STATUS_FILE, "utf-8"));
23265
23351
  } catch {
23266
23352
  }
23267
23353
  const merged = { ...existing, ...info };
23268
23354
  const tmp = STATUS_FILE + ".tmp";
23269
- fs15.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
23270
- fs15.renameSync(tmp, STATUS_FILE);
23355
+ fs16.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
23356
+ fs16.renameSync(tmp, STATUS_FILE);
23271
23357
  }
23272
23358
  function getRunningChannels() {
23273
23359
  return getStatus().adapters.map((adapter) => adapter.channelType).sort();
@@ -23276,6 +23362,24 @@ function getAdapterStatuses() {
23276
23362
  return getStatus().adapters;
23277
23363
  }
23278
23364
  async function main() {
23365
+ const lockState = tryAcquireBridgeInstanceLock();
23366
+ if (!lockState.acquired) {
23367
+ const holderPid = lockState.holderPid;
23368
+ writeStatus({
23369
+ running: true,
23370
+ ...Number.isFinite(holderPid) && holderPid ? { pid: holderPid } : {}
23371
+ });
23372
+ console.log(
23373
+ `[codex-to-im] Another bridge daemon is already running${holderPid ? ` (PID: ${holderPid})` : ""}. Exiting duplicate launcher.`
23374
+ );
23375
+ process.exit(0);
23376
+ }
23377
+ let instanceLockHeld = true;
23378
+ const releaseInstanceLock = () => {
23379
+ if (!instanceLockHeld) return;
23380
+ releaseBridgeInstanceLock(void 0, process.pid);
23381
+ instanceLockHeld = false;
23382
+ };
23279
23383
  const config2 = loadConfig();
23280
23384
  setupLogger();
23281
23385
  const runId = crypto10.randomUUID();
@@ -23294,8 +23398,8 @@ async function main() {
23294
23398
  permissions: gateway,
23295
23399
  lifecycle: {
23296
23400
  onBridgeStart: () => {
23297
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
23298
- fs15.writeFileSync(PID_FILE, String(process.pid), "utf-8");
23401
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
23402
+ fs16.writeFileSync(PID_FILE, String(process.pid), "utf-8");
23299
23403
  const channels = getRunningChannels();
23300
23404
  writeStatus({
23301
23405
  running: true,
@@ -23318,6 +23422,7 @@ async function main() {
23318
23422
  console.log(`[codex-to-im] Active channels updated: ${channels.join(", ") || "none"}`);
23319
23423
  },
23320
23424
  onBridgeStop: () => {
23425
+ releaseInstanceLock();
23321
23426
  writeStatus({ running: false, channels: [], adapters: [] });
23322
23427
  console.log("[codex-to-im] Bridge stopped");
23323
23428
  }
@@ -23332,6 +23437,7 @@ async function main() {
23332
23437
  console.log(`[codex-to-im] Shutting down (${reason})...`);
23333
23438
  pendingPerms.denyAll();
23334
23439
  await stop();
23440
+ releaseInstanceLock();
23335
23441
  writeStatus({ running: false, lastExitReason: reason });
23336
23442
  process.exit(0);
23337
23443
  };
@@ -23344,6 +23450,7 @@ async function main() {
23344
23450
  });
23345
23451
  process.on("uncaughtException", (err) => {
23346
23452
  console.error("[codex-to-im] uncaughtException:", err.stack || err.message);
23453
+ releaseInstanceLock();
23347
23454
  writeStatus({ running: false, lastExitReason: `uncaughtException: ${err.message}` });
23348
23455
  process.exit(1);
23349
23456
  });
@@ -23351,6 +23458,7 @@ async function main() {
23351
23458
  console.log(`[codex-to-im] beforeExit (code: ${code2})`);
23352
23459
  });
23353
23460
  process.on("exit", (code2) => {
23461
+ releaseInstanceLock();
23354
23462
  console.log(`[codex-to-im] exit (code: ${code2})`);
23355
23463
  });
23356
23464
  setInterval(() => {
@@ -23358,6 +23466,7 @@ async function main() {
23358
23466
  }
23359
23467
  main().catch((err) => {
23360
23468
  console.error("[codex-to-im] Fatal error:", err instanceof Error ? err.stack || err.message : err);
23469
+ releaseBridgeInstanceLock(void 0, process.pid);
23361
23470
  try {
23362
23471
  writeStatus({ running: false, lastExitReason: `fatal: ${err instanceof Error ? err.message : String(err)}` });
23363
23472
  } catch {