codex-to-im 1.0.27 → 1.0.29

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
@@ -86,6 +86,30 @@ function isProcessAlive(pid) {
86
86
  return false;
87
87
  }
88
88
  }
89
+ function collectTrackedBridgePids(bridgePid, statusPid) {
90
+ const unique = /* @__PURE__ */ new Set();
91
+ for (const pid of [bridgePid, statusPid]) {
92
+ if (Number.isFinite(pid) && pid > 0) {
93
+ unique.add(pid);
94
+ }
95
+ }
96
+ return [...unique];
97
+ }
98
+ function resolveTrackedBridgePid(bridgePid, statusPid, isAlive = isProcessAlive) {
99
+ if (isAlive(bridgePid)) return bridgePid;
100
+ if (isAlive(statusPid)) return statusPid;
101
+ return bridgePid ?? statusPid;
102
+ }
103
+ function getTrackedBridgePids(status) {
104
+ const resolvedStatus = status ?? readJsonFile(bridgeStatusFile, { running: false });
105
+ return collectTrackedBridgePids(readPid(bridgePidFile), resolvedStatus.pid);
106
+ }
107
+ function clearBridgePidFile() {
108
+ try {
109
+ fs2.unlinkSync(bridgePidFile);
110
+ } catch {
111
+ }
112
+ }
89
113
  function sleep(ms) {
90
114
  return new Promise((resolve) => setTimeout(resolve, ms));
91
115
  }
@@ -246,7 +270,7 @@ function getCurrentUiServerUrl() {
246
270
  }
247
271
  function getBridgeStatus() {
248
272
  const status = readJsonFile(bridgeStatusFile, { running: false });
249
- const pid = readPid(bridgePidFile) ?? status.pid;
273
+ const pid = resolveTrackedBridgePid(readPid(bridgePidFile), status.pid);
250
274
  if (!isProcessAlive(pid)) {
251
275
  return {
252
276
  ...status,
@@ -311,7 +335,11 @@ async function waitForUiServer(timeoutMs = 15e3) {
311
335
  async function startBridge() {
312
336
  ensureDirs();
313
337
  const current = getBridgeStatus();
314
- if (current.running) return current;
338
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive(pid));
339
+ if (current.running && extraAlivePids.length === 0) return current;
340
+ if (current.running && extraAlivePids.length > 0) {
341
+ await stopBridge();
342
+ }
315
343
  const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
316
344
  if (!fs2.existsSync(daemonEntry)) {
317
345
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
@@ -333,31 +361,38 @@ async function startBridge() {
333
361
  return status;
334
362
  }
335
363
  async function stopBridge() {
336
- const status = getBridgeStatus();
337
- if (!status.pid || !isProcessAlive(status.pid)) {
338
- return { ...status, running: false };
339
- }
340
- if (process.platform === "win32") {
341
- await new Promise((resolve) => {
342
- const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(status.pid), "/T", "/F"], {
343
- stdio: "ignore",
344
- ...WINDOWS_HIDE
364
+ const status = readJsonFile(bridgeStatusFile, { running: false });
365
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive(pid));
366
+ if (pids.length === 0) {
367
+ clearBridgePidFile();
368
+ return { ...getBridgeStatus(), running: false };
369
+ }
370
+ for (const pid of pids) {
371
+ if (process.platform === "win32") {
372
+ await new Promise((resolve) => {
373
+ const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
374
+ stdio: "ignore",
375
+ ...WINDOWS_HIDE
376
+ });
377
+ killer.on("exit", () => resolve());
378
+ killer.on("error", () => resolve());
345
379
  });
346
- killer.on("exit", () => resolve());
347
- killer.on("error", () => resolve());
348
- });
349
- } else {
350
- try {
351
- process.kill(status.pid, "SIGTERM");
352
- } catch {
380
+ } else {
381
+ try {
382
+ process.kill(pid, "SIGTERM");
383
+ } catch {
384
+ }
353
385
  }
354
386
  }
355
387
  const startedAt = Date.now();
356
388
  while (Date.now() - startedAt < 1e4) {
357
- const next = getBridgeStatus();
358
- if (!next.running) return next;
389
+ if (pids.every((pid) => !isProcessAlive(pid))) {
390
+ clearBridgePidFile();
391
+ return getBridgeStatus();
392
+ }
359
393
  await sleep(300);
360
394
  }
395
+ clearBridgePidFile();
361
396
  return getBridgeStatus();
362
397
  }
363
398
  async function getBridgeAutostartStatus() {
package/dist/daemon.mjs CHANGED
@@ -4255,6 +4255,7 @@ function getRegisteredTypes() {
4255
4255
  // src/lib/bridge/bridge-manager.ts
4256
4256
  import fs8 from "node:fs";
4257
4257
  import path11 from "node:path";
4258
+ import { inspect } from "node:util";
4258
4259
 
4259
4260
  // src/lib/bridge/adapters/telegram-adapter.ts
4260
4261
  import crypto from "crypto";
@@ -14233,12 +14234,6 @@ var WeixinAdapter = class extends BaseChannelAdapter {
14233
14234
  }
14234
14235
  }
14235
14236
  validateConfig() {
14236
- const linkedAccounts = this.filterConfiguredAccounts(
14237
- listWeixinAccounts().filter((account) => account.enabled && account.token)
14238
- );
14239
- if (linkedAccounts.length === 0) {
14240
- return "No linked WeChat account. Run the WeChat QR login helper first.";
14241
- }
14242
14237
  return null;
14243
14238
  }
14244
14239
  isAuthorized(_userId, _chatId) {
@@ -14729,8 +14724,8 @@ function walkSessionFiles(dirPath, target) {
14729
14724
  }
14730
14725
  }
14731
14726
  function isDesktopLike(meta) {
14732
- const originator = meta?.originator?.toLowerCase() || "";
14733
- const source = meta?.source?.toLowerCase() || "";
14727
+ const originator = typeof meta?.originator === "string" ? meta.originator.toLowerCase() : "";
14728
+ const source = typeof meta?.source === "string" ? meta.source.toLowerCase() : "";
14734
14729
  if (source === "exec") return false;
14735
14730
  return originator.includes("desktop") || source === "vscode" || source === "desktop";
14736
14731
  }
@@ -14862,9 +14857,9 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
14862
14857
  threadId,
14863
14858
  filePath,
14864
14859
  cwd,
14865
- originator: parsed.payload.originator || "Codex Desktop",
14866
- source: parsed.payload.source || void 0,
14867
- cliVersion: parsed.payload.cli_version || void 0,
14860
+ originator: typeof parsed.payload.originator === "string" ? parsed.payload.originator : "Codex Desktop",
14861
+ source: typeof parsed.payload.source === "string" ? parsed.payload.source : void 0,
14862
+ cliVersion: typeof parsed.payload.cli_version === "string" ? parsed.payload.cli_version : void 0,
14868
14863
  firstSeenAt,
14869
14864
  lastEventAt,
14870
14865
  title,
@@ -17018,6 +17013,9 @@ var MODE_OPTIONS_TEXT = "\u53EF\u9009\uFF1A`code`\uFF08\u76F4\u63A5\u6267\u884C\
17018
17013
  var REASONING_OPTIONS_TEXT = "\u53EF\u9009\uFF1A`1=minimal` `2=low` `3=medium` `4=high` `5=xhigh`";
17019
17014
  var DEFAULT_DESKTOP_THREAD_LIST_LIMIT = 10;
17020
17015
  var MAX_DESKTOP_THREAD_LIST_LIMIT = 200;
17016
+ var DANGLING_MIRROR_THREAD_RETRY_LIMIT = 3;
17017
+ var MIRROR_FAILURE_SUSPEND_MS = 6e4;
17018
+ var MIRROR_FAILURE_SUSPEND_THRESHOLD = 3;
17021
17019
  var MIRROR_POLL_INTERVAL_MS = 2500;
17022
17020
  var MIRROR_WATCH_DEBOUNCE_MS = 350;
17023
17021
  var MIRROR_EVENT_BATCH_LIMIT = 8;
@@ -17027,6 +17025,7 @@ var INTERACTIVE_IDLE_REMINDER_MS = 6e5;
17027
17025
  var MIRROR_IDLE_TIMEOUT_MS = 6e5;
17028
17026
  var AVAILABLE_CODEX_MODELS = listSelectableCodexModels();
17029
17027
  var AVAILABLE_CODEX_MODEL_MAP = new Map(AVAILABLE_CODEX_MODELS.map((model) => [model.slug, model]));
17028
+ var INVALID_ADAPTER_WARNING_CACHE = /* @__PURE__ */ new Map();
17030
17029
  function generateDraftId() {
17031
17030
  return Math.floor(Math.random() * 2147483646) + 1;
17032
17031
  }
@@ -17109,8 +17108,41 @@ function resolveByIndexOrPrefix(raw, items, getId) {
17109
17108
  }
17110
17109
  return { match: null, ambiguous: false };
17111
17110
  }
17111
+ function describeUnknownError(error) {
17112
+ if (error instanceof Error) {
17113
+ return error.stack || `${error.name}: ${error.message}`;
17114
+ }
17115
+ if (error === null) return "null";
17116
+ if (typeof error === "undefined") return "undefined";
17117
+ if (typeof error === "object") {
17118
+ const ctor = error?.constructor?.name;
17119
+ const rendered = inspect(error, {
17120
+ depth: 4,
17121
+ breakLength: Infinity,
17122
+ compact: true
17123
+ });
17124
+ return ctor && ctor !== "Object" ? `${ctor} ${rendered}` : rendered;
17125
+ }
17126
+ return String(error);
17127
+ }
17112
17128
  function getDisplayedDesktopThreads(limit = DEFAULT_DESKTOP_THREAD_LIST_LIMIT) {
17113
- return listDesktopSessions(limit);
17129
+ try {
17130
+ return listDesktopSessions(limit);
17131
+ } catch (error) {
17132
+ console.error("[bridge-manager] Failed to list desktop sessions:", error);
17133
+ return null;
17134
+ }
17135
+ }
17136
+ function getDesktopSessionByThreadIdSafe(threadId, context) {
17137
+ try {
17138
+ return getDesktopSessionByThreadId(threadId);
17139
+ } catch (error) {
17140
+ console.error(
17141
+ `[bridge-manager] Failed to load desktop thread ${threadId} during ${context}:`,
17142
+ error
17143
+ );
17144
+ return null;
17145
+ }
17114
17146
  }
17115
17147
  function parseDesktopThreadListArgs(args) {
17116
17148
  const trimmed = args.trim().toLowerCase();
@@ -17522,7 +17554,7 @@ async function summarizeHistory(currentBinding) {
17522
17554
  }
17523
17555
  function getDesktopThreadTitle(threadId) {
17524
17556
  if (!threadId) return null;
17525
- return getDesktopSessionByThreadId(threadId)?.title || null;
17557
+ return getDesktopSessionByThreadIdSafe(threadId, "status lookup")?.title || null;
17526
17558
  }
17527
17559
  function formatCommandPath(cwd) {
17528
17560
  return cwd?.trim() || "~";
@@ -17684,6 +17716,7 @@ function getState() {
17684
17716
  g[GLOBAL_KEY] = {
17685
17717
  adapters: /* @__PURE__ */ new Map(),
17686
17718
  adapterMeta: /* @__PURE__ */ new Map(),
17719
+ invalidAdapters: /* @__PURE__ */ new Map(),
17687
17720
  running: false,
17688
17721
  startedAt: null,
17689
17722
  loopAborts: /* @__PURE__ */ new Map(),
@@ -17706,6 +17739,9 @@ function getState() {
17706
17739
  if (!g[GLOBAL_KEY].mirrorSubscriptions) {
17707
17740
  g[GLOBAL_KEY].mirrorSubscriptions = /* @__PURE__ */ new Map();
17708
17741
  }
17742
+ if (!g[GLOBAL_KEY].invalidAdapters) {
17743
+ g[GLOBAL_KEY].invalidAdapters = /* @__PURE__ */ new Map();
17744
+ }
17709
17745
  if (!g[GLOBAL_KEY].queuedCounts) {
17710
17746
  g[GLOBAL_KEY].queuedCounts = /* @__PURE__ */ new Map();
17711
17747
  }
@@ -17802,6 +17838,20 @@ function decrementQueuedCount(sessionId) {
17802
17838
  }
17803
17839
  syncSessionRuntimeState(sessionId);
17804
17840
  }
17841
+ function resetPersistedInteractiveRuntimeState() {
17842
+ const { store } = getBridgeContext();
17843
+ for (const session of store.listSessions()) {
17844
+ const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
17845
+ if (queuedCount === 0 && session.runtime_status !== "running" && session.runtime_status !== "queued") {
17846
+ continue;
17847
+ }
17848
+ store.updateSession(session.id, {
17849
+ queued_count: 0,
17850
+ runtime_status: "idle",
17851
+ last_runtime_update_at: nowIso2()
17852
+ });
17853
+ }
17854
+ }
17805
17855
  function formatRuntimeStatus(session) {
17806
17856
  const status = session?.runtime_status || "idle";
17807
17857
  const queuedCount = session?.queued_count && session.queued_count > 0 ? session.queued_count : 0;
@@ -18051,7 +18101,9 @@ function scheduleMirrorWake(delayMs = MIRROR_WATCH_DEBOUNCE_MS) {
18051
18101
  if (state.mirrorWakeTimer) return;
18052
18102
  state.mirrorWakeTimer = setTimeout(() => {
18053
18103
  state.mirrorWakeTimer = null;
18054
- void reconcileMirrorSubscriptions();
18104
+ void reconcileMirrorSubscriptions().catch((err) => {
18105
+ console.error("[bridge-manager] Mirror wake reconcile failed:", describeUnknownError(err));
18106
+ });
18055
18107
  }, delayMs);
18056
18108
  }
18057
18109
  function watchMirrorFile(subscription, filePath) {
@@ -18089,6 +18141,16 @@ function syncMirrorSessionState(sessionId) {
18089
18141
  mirror_last_event_at: deliveredAt
18090
18142
  });
18091
18143
  }
18144
+ function syncMirrorSessionStateSafe(sessionId, context) {
18145
+ try {
18146
+ syncMirrorSessionState(sessionId);
18147
+ } catch (error) {
18148
+ console.error(
18149
+ `[bridge-manager] Failed to sync mirror session state for ${sessionId} during ${context}:`,
18150
+ describeUnknownError(error)
18151
+ );
18152
+ }
18153
+ }
18092
18154
  function getMirrorAssistantRuntimeLabel() {
18093
18155
  const { store } = getBridgeContext();
18094
18156
  const runtime = (store.getSetting("bridge_runtime") || "codex").trim().toLowerCase();
@@ -18453,7 +18515,17 @@ function removeMirrorSubscription(bindingId) {
18453
18515
  stopMirrorStreaming(existing);
18454
18516
  closeMirrorWatcher(existing);
18455
18517
  state.mirrorSubscriptions.delete(bindingId);
18456
- syncMirrorSessionState(existing.sessionId);
18518
+ syncMirrorSessionStateSafe(existing.sessionId, "mirror subscription removal");
18519
+ }
18520
+ function clearDanglingMirrorThread(subscription, reason) {
18521
+ const { store } = getBridgeContext();
18522
+ const session = store.getSession(subscription.sessionId);
18523
+ const currentThreadId = session?.sdk_session_id || subscription.threadId;
18524
+ console.warn(
18525
+ `[bridge-manager] Clearing dangling desktop thread ${currentThreadId} for session ${subscription.sessionId}: ${reason}`
18526
+ );
18527
+ store.updateSdkSessionId(subscription.sessionId, "");
18528
+ removeMirrorSubscription(subscription.bindingId);
18457
18529
  }
18458
18530
  function upsertMirrorSubscription(binding) {
18459
18531
  const { store } = getBridgeContext();
@@ -18468,7 +18540,7 @@ function upsertMirrorSubscription(binding) {
18468
18540
  removeMirrorSubscription(binding.id);
18469
18541
  return;
18470
18542
  }
18471
- const desktopSession = getDesktopSessionByThreadId(threadId);
18543
+ const desktopSession = getDesktopSessionByThreadIdSafe(threadId, "mirror subscription sync");
18472
18544
  const filePath = desktopSession?.filePath || null;
18473
18545
  const existing = state.mirrorSubscriptions.get(binding.id);
18474
18546
  if (!existing) {
@@ -18493,11 +18565,14 @@ function upsertMirrorSubscription(binding) {
18493
18565
  trailingText: "",
18494
18566
  activeMirrorTurnId: null,
18495
18567
  bufferedRecords: [],
18496
- pendingTurn: null
18568
+ pendingTurn: null,
18569
+ missingThreadPolls: 0,
18570
+ consecutiveFailures: 0,
18571
+ suspendedUntil: null
18497
18572
  };
18498
18573
  watchMirrorFile(created, filePath);
18499
18574
  state.mirrorSubscriptions.set(binding.id, created);
18500
- syncMirrorSessionState(binding.codepilotSessionId);
18575
+ syncMirrorSessionStateSafe(binding.codepilotSessionId, "mirror subscription create");
18501
18576
  return;
18502
18577
  }
18503
18578
  const previousSessionId = existing.sessionId;
@@ -18515,18 +18590,23 @@ function upsertMirrorSubscription(binding) {
18515
18590
  existing.lastDeliveredAt = session.mirror_last_event_at || null;
18516
18591
  existing.dirty = true;
18517
18592
  existing.pendingTurn = null;
18593
+ existing.missingThreadPolls = 0;
18594
+ existing.consecutiveFailures = 0;
18595
+ existing.suspendedUntil = null;
18518
18596
  resetMirrorReadState(existing);
18519
18597
  } else if (filePathChanged) {
18520
18598
  stopMirrorStreaming(existing);
18521
18599
  existing.dirty = true;
18522
18600
  existing.pendingTurn = null;
18601
+ existing.consecutiveFailures = 0;
18602
+ existing.suspendedUntil = null;
18523
18603
  resetMirrorReadState(existing);
18524
18604
  }
18525
18605
  watchMirrorFile(existing, filePath);
18526
18606
  if (previousSessionId !== binding.codepilotSessionId) {
18527
- syncMirrorSessionState(previousSessionId);
18607
+ syncMirrorSessionStateSafe(previousSessionId, "mirror subscription rebind previous session");
18528
18608
  }
18529
- syncMirrorSessionState(binding.codepilotSessionId);
18609
+ syncMirrorSessionStateSafe(binding.codepilotSessionId, "mirror subscription upsert");
18530
18610
  }
18531
18611
  function syncMirrorSubscriptionSet() {
18532
18612
  const { store } = getBridgeContext();
@@ -18540,7 +18620,14 @@ function syncMirrorSubscriptionSet() {
18540
18620
  const desiredIds = /* @__PURE__ */ new Set();
18541
18621
  for (const binding of desiredBindings) {
18542
18622
  desiredIds.add(binding.id);
18543
- upsertMirrorSubscription(binding);
18623
+ try {
18624
+ upsertMirrorSubscription(binding);
18625
+ } catch (error) {
18626
+ console.error(
18627
+ `[bridge-manager] Failed to sync mirror subscription for binding ${binding.id}:`,
18628
+ error
18629
+ );
18630
+ }
18544
18631
  }
18545
18632
  for (const bindingId of Array.from(state.mirrorSubscriptions.keys())) {
18546
18633
  if (!desiredIds.has(bindingId)) {
@@ -18555,7 +18642,29 @@ async function reconcileMirrorSubscription(subscription) {
18555
18642
  removeMirrorSubscription(subscription.bindingId);
18556
18643
  return;
18557
18644
  }
18558
- const desktopSession = getDesktopSessionByThreadId(subscription.threadId);
18645
+ if (subscription.suspendedUntil && Date.now() < subscription.suspendedUntil) {
18646
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror suspension");
18647
+ return;
18648
+ }
18649
+ if (subscription.suspendedUntil) {
18650
+ subscription.suspendedUntil = null;
18651
+ }
18652
+ const desktopSession = getDesktopSessionByThreadIdSafe(
18653
+ subscription.threadId,
18654
+ "mirror reconcile"
18655
+ );
18656
+ if (!desktopSession) {
18657
+ subscription.missingThreadPolls += 1;
18658
+ if (subscription.missingThreadPolls >= DANGLING_MIRROR_THREAD_RETRY_LIMIT) {
18659
+ clearDanglingMirrorThread(
18660
+ subscription,
18661
+ "desktop thread no longer exists locally"
18662
+ );
18663
+ return;
18664
+ }
18665
+ } else {
18666
+ subscription.missingThreadPolls = 0;
18667
+ }
18559
18668
  const filePathChanged = subscription.filePath !== (desktopSession?.filePath || null);
18560
18669
  subscription.filePath = desktopSession?.filePath || null;
18561
18670
  subscription.status = subscription.filePath ? "watching" : "stale";
@@ -18566,7 +18675,7 @@ async function reconcileMirrorSubscription(subscription) {
18566
18675
  watchMirrorFile(subscription, subscription.filePath);
18567
18676
  subscription.lastReconciledAt = nowIso2();
18568
18677
  if (!subscription.filePath) {
18569
- syncMirrorSessionState(subscription.sessionId);
18678
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile without file");
18570
18679
  return;
18571
18680
  }
18572
18681
  const snapshot = statMirrorFile(subscription.filePath);
@@ -18574,12 +18683,12 @@ async function reconcileMirrorSubscription(subscription) {
18574
18683
  subscription.status = "stale";
18575
18684
  subscription.dirty = true;
18576
18685
  resetMirrorReadState(subscription);
18577
- syncMirrorSessionState(subscription.sessionId);
18686
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile missing snapshot");
18578
18687
  return;
18579
18688
  }
18580
18689
  const unchanged = !subscription.dirty && subscription.fileIdentity === snapshot.identity && subscription.fileSize === snapshot.size && subscription.fileMtimeMs === snapshot.mtimeMs;
18581
18690
  if (unchanged && !hasPendingMirrorWork(subscription)) {
18582
- syncMirrorSessionState(subscription.sessionId);
18691
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile unchanged snapshot");
18583
18692
  return;
18584
18693
  }
18585
18694
  let deliverableRecords = [];
@@ -18634,13 +18743,13 @@ async function reconcileMirrorSubscription(subscription) {
18634
18743
  console.warn("[bridge-manager] Mirror delivery failed:", error instanceof Error ? error.message : error);
18635
18744
  }
18636
18745
  }
18637
- syncMirrorSessionState(subscription.sessionId);
18746
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile active task");
18638
18747
  return;
18639
18748
  }
18640
18749
  const finalizedTurns = timedOutTurn ? [timedOutTurn] : [];
18641
18750
  finalizedTurns.push(...consumeBufferedMirrorTurns(subscription));
18642
18751
  if (finalizedTurns.length === 0) {
18643
- syncMirrorSessionState(subscription.sessionId);
18752
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile no finalized turns");
18644
18753
  return;
18645
18754
  }
18646
18755
  try {
@@ -18649,29 +18758,66 @@ async function reconcileMirrorSubscription(subscription) {
18649
18758
  subscription.dirty = true;
18650
18759
  console.warn("[bridge-manager] Mirror delivery failed:", error instanceof Error ? error.message : error);
18651
18760
  }
18652
- syncMirrorSessionState(subscription.sessionId);
18761
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile delivered turns");
18653
18762
  }
18654
18763
  async function reconcileMirrorSubscriptions() {
18655
18764
  const state = getState();
18656
18765
  if (!state.running || state.mirrorSyncInFlight) return;
18657
18766
  state.mirrorSyncInFlight = true;
18767
+ let stage = "sync-start";
18658
18768
  try {
18659
- syncMirrorSubscriptionSet();
18660
- for (const subscription of state.mirrorSubscriptions.values()) {
18769
+ try {
18770
+ stage = "sync-subscription-set";
18771
+ syncMirrorSubscriptionSet();
18772
+ } catch (error) {
18773
+ console.error("[bridge-manager] Mirror subscription set reconcile failed:", error);
18774
+ return;
18775
+ }
18776
+ stage = "snapshot-subscriptions";
18777
+ const subscriptions = Array.from(state.mirrorSubscriptions.values());
18778
+ for (const subscription of subscriptions) {
18779
+ stage = `subscription:${subscription.bindingId}`;
18661
18780
  try {
18662
18781
  await reconcileMirrorSubscription(subscription);
18782
+ subscription.consecutiveFailures = 0;
18783
+ subscription.suspendedUntil = null;
18663
18784
  } catch (error) {
18664
- stopMirrorStreaming(subscription, "interrupted");
18665
- resetMirrorReadState(subscription);
18666
- subscription.status = "stale";
18667
- subscription.dirty = false;
18668
- console.error(
18669
- `[bridge-manager] Mirror reconcile failed for thread ${subscription.threadId}:`,
18670
- error instanceof Error ? error.stack || error.message : error
18671
- );
18672
- syncMirrorSessionState(subscription.sessionId);
18785
+ try {
18786
+ stopMirrorStreaming(subscription, "interrupted");
18787
+ subscription.pendingTurn = null;
18788
+ subscription.bufferedRecords = [];
18789
+ subscription.status = "stale";
18790
+ subscription.dirty = false;
18791
+ subscription.consecutiveFailures += 1;
18792
+ if (subscription.consecutiveFailures >= MIRROR_FAILURE_SUSPEND_THRESHOLD) {
18793
+ subscription.suspendedUntil = Date.now() + MIRROR_FAILURE_SUSPEND_MS;
18794
+ console.warn(
18795
+ `[bridge-manager] Mirror subscription for thread ${subscription.threadId} is suspended for ${Math.round(MIRROR_FAILURE_SUSPEND_MS / 1e3)}s after ${subscription.consecutiveFailures} consecutive failures`
18796
+ );
18797
+ }
18798
+ console.error(
18799
+ `[bridge-manager] Mirror reconcile failed for thread ${subscription.threadId}:`,
18800
+ describeUnknownError(error)
18801
+ );
18802
+ syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile failure");
18803
+ } catch (recoveryError) {
18804
+ console.error(
18805
+ `[bridge-manager] Mirror reconcile recovery failed for thread ${subscription.threadId}:`,
18806
+ describeUnknownError(recoveryError)
18807
+ );
18808
+ console.error(
18809
+ `[bridge-manager] Original mirror reconcile error for thread ${subscription.threadId}:`,
18810
+ describeUnknownError(error)
18811
+ );
18812
+ }
18673
18813
  }
18674
18814
  }
18815
+ stage = "sync-complete";
18816
+ } catch (error) {
18817
+ console.error(
18818
+ `[bridge-manager] Mirror reconcile failed during ${stage}:`,
18819
+ describeUnknownError(error)
18820
+ );
18675
18821
  } finally {
18676
18822
  state.mirrorSyncInFlight = false;
18677
18823
  }
@@ -18760,6 +18906,8 @@ function listEnabledAdapterInstances() {
18760
18906
  async function stopAdapterInstance(channelType) {
18761
18907
  const state = getState();
18762
18908
  const adapter = state.adapters.get(channelType);
18909
+ state.invalidAdapters.delete(channelType);
18910
+ INVALID_ADAPTER_WARNING_CACHE.delete(channelType);
18763
18911
  if (!adapter) return;
18764
18912
  state.loopAborts.get(channelType)?.abort();
18765
18913
  state.loopAborts.delete(channelType);
@@ -18785,6 +18933,14 @@ async function syncConfiguredAdapters(options) {
18785
18933
  await stopAdapterInstance(existingKey);
18786
18934
  changed = true;
18787
18935
  }
18936
+ for (const invalidKey of Array.from(state.invalidAdapters.keys())) {
18937
+ if (desiredKeys.has(invalidKey)) continue;
18938
+ state.invalidAdapters.delete(invalidKey);
18939
+ }
18940
+ for (const invalidKey of Array.from(INVALID_ADAPTER_WARNING_CACHE.keys())) {
18941
+ if (desiredKeys.has(invalidKey)) continue;
18942
+ INVALID_ADAPTER_WARNING_CACHE.delete(invalidKey);
18943
+ }
18788
18944
  for (const instance of desiredInstances) {
18789
18945
  const existing = state.adapters.get(instance.id);
18790
18946
  const desiredFingerprint = desiredFingerprints.get(instance.id) || "";
@@ -18798,9 +18954,16 @@ async function syncConfiguredAdapters(options) {
18798
18954
  if (!adapter) continue;
18799
18955
  const configError = adapter.validateConfig();
18800
18956
  if (configError) {
18801
- console.warn(`[bridge-manager] ${instance.id} adapter not valid:`, configError);
18957
+ const invalidSignature = `${desiredFingerprint}:${configError}`;
18958
+ if (INVALID_ADAPTER_WARNING_CACHE.get(instance.id) !== invalidSignature) {
18959
+ console.warn(`[bridge-manager] ${instance.id} adapter not valid:`, configError);
18960
+ INVALID_ADAPTER_WARNING_CACHE.set(instance.id, invalidSignature);
18961
+ state.invalidAdapters.set(instance.id, invalidSignature);
18962
+ }
18802
18963
  continue;
18803
18964
  }
18965
+ state.invalidAdapters.delete(instance.id);
18966
+ INVALID_ADAPTER_WARNING_CACHE.delete(instance.id);
18804
18967
  try {
18805
18968
  state.adapters.set(instance.id, adapter);
18806
18969
  state.adapterMeta.set(instance.id, {
@@ -18833,6 +18996,7 @@ async function start() {
18833
18996
  console.log("[bridge-manager] Bridge not enabled (remote_bridge_enabled != true)");
18834
18997
  return;
18835
18998
  }
18999
+ resetPersistedInteractiveRuntimeState();
18836
19000
  await syncConfiguredAdapters({ startLoops: false });
18837
19001
  const startedCount = state.adapters.size;
18838
19002
  if (startedCount === 0) {
@@ -18859,11 +19023,11 @@ async function start() {
18859
19023
  }, 5e3);
18860
19024
  state.mirrorPollTimer = setInterval(() => {
18861
19025
  void reconcileMirrorSubscriptions().catch((err) => {
18862
- console.error("[bridge-manager] Mirror reconcile failed:", err);
19026
+ console.error("[bridge-manager] Mirror reconcile failed:", describeUnknownError(err));
18863
19027
  });
18864
19028
  }, MIRROR_POLL_INTERVAL_MS);
18865
19029
  void reconcileMirrorSubscriptions().catch((err) => {
18866
- console.error("[bridge-manager] Initial mirror reconcile failed:", err);
19030
+ console.error("[bridge-manager] Initial mirror reconcile failed:", describeUnknownError(err));
18867
19031
  });
18868
19032
  console.log(`[bridge-manager] Bridge started with ${startedCount} adapter(s)`);
18869
19033
  }
@@ -18896,6 +19060,8 @@ async function stop() {
18896
19060
  state.mirrorSuppressUntil.clear();
18897
19061
  state.mirrorIgnoredTurnIds.clear();
18898
19062
  state.queuedCounts.clear();
19063
+ state.invalidAdapters.clear();
19064
+ INVALID_ADAPTER_WARNING_CACHE.clear();
18899
19065
  for (const sessionId of activeSessionIds) {
18900
19066
  syncSessionRuntimeState(sessionId);
18901
19067
  }
@@ -19388,6 +19554,10 @@ async function handleCommand(adapter, msg, text2) {
19388
19554
  }
19389
19555
  if (args === "all") {
19390
19556
  const desktopSessions = getDisplayedDesktopThreads(MAX_DESKTOP_THREAD_LIST_LIMIT);
19557
+ if (!desktopSessions) {
19558
+ response = "\u8BFB\u53D6\u684C\u9762\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
19559
+ break;
19560
+ }
19391
19561
  if (desktopSessions.length === 0) {
19392
19562
  response = "\u6CA1\u6709\u627E\u5230\u684C\u9762\u4F1A\u8BDD\u3002\u5148\u5728 Codex Desktop App \u4E2D\u6253\u5F00\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u518D\u56DE\u6765\u8BD5\u4E00\u6B21\u3002";
19393
19563
  break;
@@ -19400,6 +19570,10 @@ async function handleCommand(adapter, msg, text2) {
19400
19570
  break;
19401
19571
  }
19402
19572
  const displayedThreads = getDisplayedDesktopThreads(MAX_DESKTOP_THREAD_LIST_LIMIT);
19573
+ if (!displayedThreads) {
19574
+ response = "\u8BFB\u53D6\u684C\u9762\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
19575
+ break;
19576
+ }
19403
19577
  const threadPick = resolveByIndexOrPrefix(args, displayedThreads, (session) => session.threadId);
19404
19578
  if (threadPick.ambiguous) {
19405
19579
  response = "\u5339\u914D\u5230\u591A\u4E2A\u684C\u9762\u4F1A\u8BDD\uFF0C\u8BF7\u5148\u53D1\u9001 `/t` \u67E5\u770B\u5217\u8868\uFF0C\u518D\u7528 `/t 1` \u8FD9\u79CD\u5E8F\u53F7\u5207\u6362\u3002";
@@ -19407,7 +19581,7 @@ async function handleCommand(adapter, msg, text2) {
19407
19581
  }
19408
19582
  if (!threadPick.match) {
19409
19583
  if (validateSessionId(args)) {
19410
- const desktop = getDesktopSessionByThreadId(args);
19584
+ const desktop = getDesktopSessionByThreadIdSafe(args, "thread switch");
19411
19585
  let binding2;
19412
19586
  try {
19413
19587
  binding2 = bindToSdkSession(msg.address, args, desktop ? {
@@ -19466,6 +19640,10 @@ async function handleCommand(adapter, msg, text2) {
19466
19640
  }
19467
19641
  const { showAll, limit } = listArgs;
19468
19642
  const desktopSessions = getDisplayedDesktopThreads(limit);
19643
+ if (!desktopSessions) {
19644
+ response = "\u8BFB\u53D6\u684C\u9762\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
19645
+ break;
19646
+ }
19469
19647
  if (desktopSessions.length === 0) {
19470
19648
  response = showAll ? "\u6CA1\u6709\u627E\u5230\u684C\u9762\u4F1A\u8BDD\u3002\u5148\u5728 Codex Desktop App \u4E2D\u6253\u5F00\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u518D\u56DE\u6765\u8BD5\u4E00\u6B21\u3002" : "\u6CA1\u6709\u627E\u5230\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD\u3002\u5148\u5728 Codex Desktop App \u4E2D\u6253\u5F00\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u518D\u56DE\u6765\u8BD5\u4E00\u6B21\u3002";
19471
19649
  break;
@@ -20872,6 +21050,7 @@ var PendingPermissions = class {
20872
21050
  // src/logger.ts
20873
21051
  import fs11 from "node:fs";
20874
21052
  import path13 from "node:path";
21053
+ import { inspect as inspect2 } from "node:util";
20875
21054
  var MASK_PATTERNS = [
20876
21055
  /(?:token|secret|password|api_key)["']?\s*[:=]\s*["']?([^\s"',]+)/gi,
20877
21056
  /bot\d+:[A-Za-z0-9_-]{35}/g,
@@ -20893,6 +21072,22 @@ var LOG_PATH = path13.join(LOG_DIR, "bridge.log");
20893
21072
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
20894
21073
  var MAX_ROTATED = 3;
20895
21074
  var logStream = null;
21075
+ function formatLogArg(value) {
21076
+ if (typeof value === "string") return value;
21077
+ if (value instanceof Error) {
21078
+ return value.stack || `${value.name}: ${value.message}`;
21079
+ }
21080
+ if (value === null) return "null";
21081
+ if (typeof value === "undefined") return "undefined";
21082
+ if (typeof value === "object") {
21083
+ return inspect2(value, {
21084
+ depth: 4,
21085
+ breakLength: Infinity,
21086
+ compact: true
21087
+ });
21088
+ }
21089
+ return String(value);
21090
+ }
20896
21091
  function openLogStream() {
20897
21092
  return fs11.createWriteStream(LOG_PATH, { flags: "a" });
20898
21093
  }
@@ -20922,7 +21117,7 @@ function setupLogger() {
20922
21117
  logStream = openLogStream();
20923
21118
  const write = (level, args) => {
20924
21119
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
20925
- const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
21120
+ const message = args.map((a) => formatLogArg(a)).join(" ");
20926
21121
  const formatted = `[${timestamp}] [${level}] ${message}`;
20927
21122
  const masked = maskSecrets(formatted);
20928
21123
  rotateIfNeeded();
@@ -5142,8 +5142,8 @@ function walkSessionFiles(dirPath, target) {
5142
5142
  }
5143
5143
  }
5144
5144
  function isDesktopLike(meta) {
5145
- const originator = meta?.originator?.toLowerCase() || "";
5146
- const source = meta?.source?.toLowerCase() || "";
5145
+ const originator = typeof meta?.originator === "string" ? meta.originator.toLowerCase() : "";
5146
+ const source = typeof meta?.source === "string" ? meta.source.toLowerCase() : "";
5147
5147
  if (source === "exec") return false;
5148
5148
  return originator.includes("desktop") || source === "vscode" || source === "desktop";
5149
5149
  }
@@ -5275,9 +5275,9 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
5275
5275
  threadId,
5276
5276
  filePath,
5277
5277
  cwd,
5278
- originator: parsed.payload.originator || "Codex Desktop",
5279
- source: parsed.payload.source || void 0,
5280
- cliVersion: parsed.payload.cli_version || void 0,
5278
+ originator: typeof parsed.payload.originator === "string" ? parsed.payload.originator : "Codex Desktop",
5279
+ source: typeof parsed.payload.source === "string" ? parsed.payload.source : void 0,
5280
+ cliVersion: typeof parsed.payload.cli_version === "string" ? parsed.payload.cli_version : void 0,
5281
5281
  firstSeenAt,
5282
5282
  lastEventAt,
5283
5283
  title,
@@ -5646,6 +5646,30 @@ function isProcessAlive(pid) {
5646
5646
  return false;
5647
5647
  }
5648
5648
  }
5649
+ function collectTrackedBridgePids(bridgePid, statusPid) {
5650
+ const unique = /* @__PURE__ */ new Set();
5651
+ for (const pid of [bridgePid, statusPid]) {
5652
+ if (Number.isFinite(pid) && pid > 0) {
5653
+ unique.add(pid);
5654
+ }
5655
+ }
5656
+ return [...unique];
5657
+ }
5658
+ function resolveTrackedBridgePid(bridgePid, statusPid, isAlive = isProcessAlive) {
5659
+ if (isAlive(bridgePid)) return bridgePid;
5660
+ if (isAlive(statusPid)) return statusPid;
5661
+ return bridgePid ?? statusPid;
5662
+ }
5663
+ function getTrackedBridgePids(status) {
5664
+ const resolvedStatus = status ?? readJsonFile(bridgeStatusFile, { running: false });
5665
+ return collectTrackedBridgePids(readPid(bridgePidFile), resolvedStatus.pid);
5666
+ }
5667
+ function clearBridgePidFile() {
5668
+ try {
5669
+ fs3.unlinkSync(bridgePidFile);
5670
+ } catch {
5671
+ }
5672
+ }
5649
5673
  function sleep(ms) {
5650
5674
  return new Promise((resolve) => setTimeout(resolve, ms));
5651
5675
  }
@@ -5700,7 +5724,7 @@ function getUiServerUrl(port2 = uiPort) {
5700
5724
  }
5701
5725
  function getBridgeStatus() {
5702
5726
  const status = readJsonFile(bridgeStatusFile, { running: false });
5703
- const pid = readPid(bridgePidFile) ?? status.pid;
5727
+ const pid = resolveTrackedBridgePid(readPid(bridgePidFile), status.pid);
5704
5728
  if (!isProcessAlive(pid)) {
5705
5729
  return {
5706
5730
  ...status,
@@ -5750,7 +5774,11 @@ async function waitForBridgeRunning(timeoutMs = 2e4) {
5750
5774
  async function startBridge() {
5751
5775
  ensureDirs();
5752
5776
  const current = getBridgeStatus();
5753
- if (current.running) return current;
5777
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive(pid));
5778
+ if (current.running && extraAlivePids.length === 0) return current;
5779
+ if (current.running && extraAlivePids.length > 0) {
5780
+ await stopBridge();
5781
+ }
5754
5782
  const daemonEntry = path4.join(packageRoot, "dist", "daemon.mjs");
5755
5783
  if (!fs3.existsSync(daemonEntry)) {
5756
5784
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
@@ -5772,31 +5800,38 @@ async function startBridge() {
5772
5800
  return status;
5773
5801
  }
5774
5802
  async function stopBridge() {
5775
- const status = getBridgeStatus();
5776
- if (!status.pid || !isProcessAlive(status.pid)) {
5777
- return { ...status, running: false };
5778
- }
5779
- if (process.platform === "win32") {
5780
- await new Promise((resolve) => {
5781
- const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(status.pid), "/T", "/F"], {
5782
- stdio: "ignore",
5783
- ...WINDOWS_HIDE
5803
+ const status = readJsonFile(bridgeStatusFile, { running: false });
5804
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive(pid));
5805
+ if (pids.length === 0) {
5806
+ clearBridgePidFile();
5807
+ return { ...getBridgeStatus(), running: false };
5808
+ }
5809
+ for (const pid of pids) {
5810
+ if (process.platform === "win32") {
5811
+ await new Promise((resolve) => {
5812
+ const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
5813
+ stdio: "ignore",
5814
+ ...WINDOWS_HIDE
5815
+ });
5816
+ killer.on("exit", () => resolve());
5817
+ killer.on("error", () => resolve());
5784
5818
  });
5785
- killer.on("exit", () => resolve());
5786
- killer.on("error", () => resolve());
5787
- });
5788
- } else {
5789
- try {
5790
- process.kill(status.pid, "SIGTERM");
5791
- } catch {
5819
+ } else {
5820
+ try {
5821
+ process.kill(pid, "SIGTERM");
5822
+ } catch {
5823
+ }
5792
5824
  }
5793
5825
  }
5794
5826
  const startedAt = Date.now();
5795
5827
  while (Date.now() - startedAt < 1e4) {
5796
- const next = getBridgeStatus();
5797
- if (!next.running) return next;
5828
+ if (pids.every((pid) => !isProcessAlive(pid))) {
5829
+ clearBridgePidFile();
5830
+ return getBridgeStatus();
5831
+ }
5798
5832
  await sleep(300);
5799
5833
  }
5834
+ clearBridgePidFile();
5800
5835
  return getBridgeStatus();
5801
5836
  }
5802
5837
  async function restartBridge() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",