codex-to-im 1.0.58 → 1.0.60

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/daemon.mjs CHANGED
@@ -321,11 +321,50 @@ var init_sse_utils = __esm({
321
321
  // src/codex-provider.ts
322
322
  var codex_provider_exports = {};
323
323
  __export(codex_provider_exports, {
324
- CodexProvider: () => CodexProvider
324
+ CodexProvider: () => CodexProvider,
325
+ mapCodexUsage: () => mapCodexUsage
325
326
  });
326
- import fs14 from "node:fs";
327
+ import fs15 from "node:fs";
327
328
  import os4 from "node:os";
328
329
  import path17 from "node:path";
330
+ function isRecord(value) {
331
+ return typeof value === "object" && value !== null && !Array.isArray(value);
332
+ }
333
+ function normalizeTokenCount(value) {
334
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
335
+ }
336
+ function mapCodexUsage(usage) {
337
+ if (!isRecord(usage)) return void 0;
338
+ const cacheRead = usage.cached_input_tokens ?? usage.cache_read_input_tokens;
339
+ const cacheCreation = usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens;
340
+ return {
341
+ input_tokens: normalizeTokenCount(usage.input_tokens),
342
+ output_tokens: normalizeTokenCount(usage.output_tokens),
343
+ cache_read_input_tokens: normalizeTokenCount(cacheRead),
344
+ reasoning_output_tokens: normalizeTokenCount(usage.reasoning_output_tokens),
345
+ ...cacheCreation === void 0 ? {} : { cache_creation_input_tokens: normalizeTokenCount(cacheCreation) }
346
+ };
347
+ }
348
+ function isMissingCodexPackageError(error) {
349
+ if (!isRecord(error)) return false;
350
+ const code2 = typeof error.code === "string" ? error.code : "";
351
+ const message = typeof error.message === "string" ? error.message : "";
352
+ return (code2 === "ERR_MODULE_NOT_FOUND" || code2 === "MODULE_NOT_FOUND") && /@openai[\\/]codex(?:-sdk)?/.test(message);
353
+ }
354
+ function assertCodexClient(value) {
355
+ if (!isRecord(value) || typeof value.startThread !== "function" || typeof value.resumeThread !== "function") {
356
+ throw new Error(
357
+ "[CodexProvider] Incompatible @openai/codex-sdk: Codex client does not expose startThread() and resumeThread()."
358
+ );
359
+ }
360
+ }
361
+ function assertCodexThread(value) {
362
+ if (!isRecord(value) || typeof value.runStreamed !== "function") {
363
+ throw new Error(
364
+ "[CodexProvider] Incompatible @openai/codex-sdk: thread does not expose runStreamed()."
365
+ );
366
+ }
367
+ }
329
368
  function toApprovalPolicy(permissionMode) {
330
369
  switch (permissionMode) {
331
370
  case "never":
@@ -441,20 +480,37 @@ var init_codex_provider = __esm({
441
480
  if (this.sdk && this.codex) {
442
481
  return { sdk: this.sdk, codex: this.codex };
443
482
  }
483
+ let sdk;
444
484
  try {
445
- this.sdk = await Function('return import("@openai/codex-sdk")')();
446
- } catch {
485
+ sdk = await Function('return import("@openai/codex-sdk")')();
486
+ } catch (error) {
487
+ if (isMissingCodexPackageError(error)) {
488
+ throw new Error(
489
+ "[CodexProvider] @openai/codex-sdk is missing from this codex-to-im installation. Reinstall codex-to-im or run npm install in the project root.",
490
+ { cause: error }
491
+ );
492
+ }
493
+ const detail = error instanceof Error ? error.message : String(error);
447
494
  throw new Error(
448
- "[CodexProvider] @openai/codex-sdk is missing from this codex-to-im installation. Reinstall codex-to-im or run npm install in the project root."
495
+ `[CodexProvider] Failed to load @openai/codex-sdk: ${detail}`,
496
+ { cause: error }
449
497
  );
450
498
  }
499
+ if (!sdk || typeof sdk.Codex !== "function") {
500
+ throw new Error(
501
+ "[CodexProvider] Incompatible @openai/codex-sdk: Codex export is unavailable."
502
+ );
503
+ }
504
+ this.sdk = sdk;
451
505
  const apiKey = process.env.CTI_CODEX_API_KEY || process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || void 0;
452
506
  const baseUrl = process.env.CTI_CODEX_BASE_URL || void 0;
453
- const CodexClass = this.sdk.Codex;
454
- this.codex = new CodexClass({
507
+ const CodexClass = sdk.Codex;
508
+ const codex = new CodexClass({
455
509
  ...apiKey ? { apiKey } : {},
456
510
  ...baseUrl ? { baseUrl } : {}
457
511
  });
512
+ assertCodexClient(codex);
513
+ this.codex = codex;
458
514
  return { sdk: this.sdk, codex: this.codex };
459
515
  }
460
516
  streamChat(params) {
@@ -487,13 +543,13 @@ var init_codex_provider = __esm({
487
543
  { type: "text", text: params.prompt }
488
544
  ];
489
545
  for (const file of imageFiles) {
490
- if (file.filePath && fs14.existsSync(file.filePath)) {
546
+ if (file.filePath && fs15.existsSync(file.filePath)) {
491
547
  parts.push({ type: "local_image", path: file.filePath });
492
548
  continue;
493
549
  }
494
550
  const ext = MIME_EXT[file.type] || ".png";
495
551
  const tmpPath = path17.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
496
- fs14.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
552
+ fs15.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
497
553
  tempFiles.push(tmpPath);
498
554
  parts.push({ type: "local_image", path: tmpPath });
499
555
  }
@@ -514,6 +570,7 @@ var init_codex_provider = __esm({
514
570
  } else {
515
571
  thread = codex.startThread(threadOptions);
516
572
  }
573
+ assertCodexThread(thread);
517
574
  let sawAnyEvent = false;
518
575
  let sawTerminalEvent = false;
519
576
  let sawCompletedAssistantContent = false;
@@ -581,15 +638,9 @@ var init_codex_provider = __esm({
581
638
  break;
582
639
  }
583
640
  case "turn.completed": {
584
- const usage = event.usage;
585
641
  const threadId = self.threadIds.get(params.sessionId);
586
642
  controller.enqueue(sseEvent("result", {
587
- usage: usage ? {
588
- input_tokens: usage.input_tokens ?? 0,
589
- output_tokens: usage.output_tokens ?? 0,
590
- cache_read_input_tokens: usage.cached_input_tokens ?? 0,
591
- reasoning_output_tokens: usage.reasoning_output_tokens ?? 0
592
- } : void 0,
643
+ usage: mapCodexUsage(event.usage),
593
644
  ...threadId ? { session_id: threadId } : {}
594
645
  }));
595
646
  sawTerminalEvent = true;
@@ -610,10 +661,9 @@ var init_codex_provider = __esm({
610
661
  break;
611
662
  }
612
663
  default: {
613
- const exhaustiveEvent = event;
614
664
  console.warn(
615
665
  "[codex-provider] Unhandled thread event:",
616
- stringifyUnknown(exhaustiveEvent)
666
+ stringifyUnknown(event)
617
667
  );
618
668
  break;
619
669
  }
@@ -665,7 +715,7 @@ var init_codex_provider = __esm({
665
715
  } finally {
666
716
  for (const tmp of tempFiles) {
667
717
  try {
668
- fs14.unlinkSync(tmp);
718
+ fs15.unlinkSync(tmp);
669
719
  } catch {
670
720
  }
671
721
  }
@@ -782,10 +832,9 @@ var init_codex_provider = __esm({
782
832
  break;
783
833
  }
784
834
  default: {
785
- const exhaustiveItem = item;
786
835
  console.warn(
787
836
  "[codex-provider] Unhandled thread item:",
788
- stringifyUnknown(exhaustiveItem)
837
+ stringifyUnknown(item)
789
838
  );
790
839
  break;
791
840
  }
@@ -796,9 +845,9 @@ var init_codex_provider = __esm({
796
845
  });
797
846
 
798
847
  // src/main.ts
799
- import fs15 from "node:fs";
848
+ import fs16 from "node:fs";
800
849
  import path18 from "node:path";
801
- import crypto7 from "node:crypto";
850
+ import crypto10 from "node:crypto";
802
851
 
803
852
  // src/lib/bridge/context.ts
804
853
  var CONTEXT_KEY = "__bridge_context__";
@@ -815,23 +864,175 @@ function getBridgeContext() {
815
864
  return ctx;
816
865
  }
817
866
 
867
+ // src/lib/bridge/channel-adapter.ts
868
+ function buildInboundDedupKey(channelType, messageId) {
869
+ return `inbound:${channelType}:${messageId}`;
870
+ }
871
+ var BaseChannelAdapter = class {
872
+ inboundQueue = [];
873
+ inboundWaiters = [];
874
+ /** Human-readable instance alias, when applicable. */
875
+ alias;
876
+ /**
877
+ * Answer a callback query or interactive-card action when supported.
878
+ * Not all platforms support this — default implementation is a no-op.
879
+ */
880
+ async answerCallback(_callbackQueryId, _text) {
881
+ }
882
+ consumeInboundMessage(isRunning) {
883
+ const queued = this.inboundQueue.shift();
884
+ if (queued) return Promise.resolve(queued);
885
+ if (!isRunning) return Promise.resolve(null);
886
+ return new Promise((resolve2) => {
887
+ this.inboundWaiters.push(resolve2);
888
+ });
889
+ }
890
+ enqueueInboundMessage(message) {
891
+ const waiter = this.inboundWaiters.shift();
892
+ if (waiter) {
893
+ waiter(message);
894
+ } else {
895
+ this.inboundQueue.push(message);
896
+ }
897
+ }
898
+ clearInboundQueue() {
899
+ this.inboundQueue = [];
900
+ }
901
+ rejectPendingInboundConsumers() {
902
+ for (const waiter of this.inboundWaiters) {
903
+ waiter(null);
904
+ }
905
+ this.inboundWaiters = [];
906
+ }
907
+ };
908
+ var adapterFactories = /* @__PURE__ */ new Map();
909
+ function registerAdapterFactory(provider, factory) {
910
+ adapterFactories.set(provider, factory);
911
+ }
912
+ function createAdapter(instance) {
913
+ const factory = adapterFactories.get(instance.provider);
914
+ return factory ? factory(instance) : null;
915
+ }
916
+
818
917
  // src/lib/bridge/bridge-manager.ts
819
918
  import { inspect } from "node:util";
820
919
 
821
920
  // src/lib/bridge/adapters/feishu-adapter.ts
822
- import crypto from "crypto";
823
- import fs2 from "node:fs";
921
+ import crypto3 from "crypto";
922
+ import fs3 from "node:fs";
824
923
  import path2 from "node:path";
825
924
  import * as lark from "@larksuiteoapi/node-sdk";
826
925
 
827
926
  // src/config.ts
828
927
  init_runtime_options();
829
- import fs from "node:fs";
928
+ import fs2 from "node:fs";
929
+ import crypto2 from "node:crypto";
830
930
  import os from "node:os";
831
931
  import path from "node:path";
932
+
933
+ // src/file-lock.ts
934
+ import crypto from "node:crypto";
935
+ import fs from "node:fs";
936
+ var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
937
+ var DEFAULT_STALE_LOCK_MS = 3e4;
938
+ var LOCK_RETRY_INTERVAL_MS = 25;
939
+ var WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
940
+ var ACTIVE_LOCKS = /* @__PURE__ */ new Map();
941
+ function sleepSync(ms) {
942
+ Atomics.wait(WAIT_ARRAY, 0, 0, Math.max(1, ms));
943
+ }
944
+ function isProcessAlive(pid) {
945
+ if (!Number.isInteger(pid) || pid <= 0) return false;
946
+ try {
947
+ process.kill(pid, 0);
948
+ return true;
949
+ } catch (error) {
950
+ return error.code === "EPERM";
951
+ }
952
+ }
953
+ function removeStaleLock(lockPath, staleAfterMs) {
954
+ try {
955
+ const stat = fs.statSync(lockPath);
956
+ if (Date.now() - stat.mtimeMs < staleAfterMs) return false;
957
+ let ownerPid = 0;
958
+ try {
959
+ const parsed = JSON.parse(fs.readFileSync(lockPath, "utf8"));
960
+ ownerPid = typeof parsed.pid === "number" ? parsed.pid : 0;
961
+ } catch {
962
+ }
963
+ if (ownerPid && isProcessAlive(ownerPid)) return false;
964
+ fs.rmSync(lockPath, { force: true });
965
+ return true;
966
+ } catch (error) {
967
+ return error.code === "ENOENT";
968
+ }
969
+ }
970
+ function withFileLock(targetPath, operation, options) {
971
+ const lockPath = `${targetPath}.lock`;
972
+ const activeDepth = ACTIVE_LOCKS.get(lockPath) || 0;
973
+ if (activeDepth > 0) {
974
+ ACTIVE_LOCKS.set(lockPath, activeDepth + 1);
975
+ try {
976
+ return operation();
977
+ } finally {
978
+ const nextDepth = (ACTIVE_LOCKS.get(lockPath) || 1) - 1;
979
+ if (nextDepth > 0) ACTIVE_LOCKS.set(lockPath, nextDepth);
980
+ else ACTIVE_LOCKS.delete(lockPath);
981
+ }
982
+ }
983
+ const timeoutMs = Math.max(0, options?.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS);
984
+ const staleAfterMs = Math.max(timeoutMs, options?.staleAfterMs ?? DEFAULT_STALE_LOCK_MS);
985
+ const deadline = Date.now() + timeoutMs;
986
+ const record = {
987
+ token: crypto.randomUUID(),
988
+ pid: process.pid,
989
+ createdAt: Date.now()
990
+ };
991
+ let handle = null;
992
+ while (handle === null) {
993
+ try {
994
+ handle = fs.openSync(lockPath, "wx", 384);
995
+ fs.writeFileSync(handle, JSON.stringify(record), "utf8");
996
+ } catch (error) {
997
+ const code2 = error.code;
998
+ if (code2 !== "EEXIST") throw error;
999
+ if (removeStaleLock(lockPath, staleAfterMs)) continue;
1000
+ if (Date.now() >= deadline) {
1001
+ throw new Error(`Timed out waiting for file lock: ${lockPath}`);
1002
+ }
1003
+ sleepSync(Math.min(LOCK_RETRY_INTERVAL_MS, Math.max(1, deadline - Date.now())));
1004
+ }
1005
+ }
1006
+ try {
1007
+ ACTIVE_LOCKS.set(lockPath, 1);
1008
+ return operation();
1009
+ } finally {
1010
+ ACTIVE_LOCKS.delete(lockPath);
1011
+ try {
1012
+ fs.closeSync(handle);
1013
+ } catch {
1014
+ }
1015
+ try {
1016
+ const current = JSON.parse(fs.readFileSync(lockPath, "utf8"));
1017
+ if (current.token === record.token) fs.rmSync(lockPath, { force: true });
1018
+ } catch {
1019
+ }
1020
+ }
1021
+ }
1022
+ function withFileLocks(targetPaths, operation) {
1023
+ const paths = Array.from(new Set(targetPaths)).sort();
1024
+ const acquire = (index) => {
1025
+ if (index >= paths.length) return operation();
1026
+ return withFileLock(paths[index], () => acquire(index + 1));
1027
+ };
1028
+ return acquire(0);
1029
+ }
1030
+
1031
+ // src/config.ts
832
1032
  function isSupportedChannelProvider(value) {
833
1033
  return value === "feishu" || value === "weixin";
834
1034
  }
1035
+ var RAW_CHANNELS = Symbol("rawConfigV2Channels");
835
1036
  function toFeishuConfig(channel) {
836
1037
  return channel?.provider === "feishu" ? channel.config : void 0;
837
1038
  }
@@ -871,7 +1072,7 @@ function parseEnvFile(content) {
871
1072
  }
872
1073
  function loadRawConfigEnv() {
873
1074
  try {
874
- return parseEnvFile(fs.readFileSync(CONFIG_PATH, "utf-8"));
1075
+ return parseEnvFile(fs2.readFileSync(CONFIG_PATH, "utf-8"));
875
1076
  } catch {
876
1077
  return /* @__PURE__ */ new Map();
877
1078
  }
@@ -901,26 +1102,66 @@ function nowIso() {
901
1102
  return (/* @__PURE__ */ new Date()).toISOString();
902
1103
  }
903
1104
  function ensureConfigDir() {
904
- fs.mkdirSync(CTI_HOME, { recursive: true });
1105
+ fs2.mkdirSync(CTI_HOME, { recursive: true });
905
1106
  }
906
1107
  function readConfigV2File() {
1108
+ if (!fs2.existsSync(CONFIG_V2_PATH)) return null;
1109
+ let content;
1110
+ try {
1111
+ content = fs2.readFileSync(CONFIG_V2_PATH, "utf-8");
1112
+ } catch (error) {
1113
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH}: ${error instanceof Error ? error.message : String(error)}`);
1114
+ }
1115
+ let raw;
1116
+ try {
1117
+ raw = JSON.parse(content);
1118
+ } catch (error) {
1119
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6: ${error instanceof Error ? error.message : String(error)}`);
1120
+ }
1121
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1122
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7684\u6839\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
1123
+ }
1124
+ if (raw.schemaVersion !== 2) {
1125
+ throw new Error(`\u4E0D\u652F\u6301\u914D\u7F6E schemaVersion=${String(raw.schemaVersion)}\uFF0C\u5DF2\u62D2\u7EDD\u7528\u5F53\u524D\u7248\u672C\u8986\u76D6\u3002`);
1126
+ }
1127
+ if (!raw.runtime || typeof raw.runtime !== "object" || Array.isArray(raw.runtime)) {
1128
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11\u6709\u6548\u7684 runtime \u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
1129
+ }
1130
+ if (!Array.isArray(raw.channels)) {
1131
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11 channels \u6570\u7EC4\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
1132
+ }
1133
+ const rawChannels = raw.channels;
1134
+ const parsed = {
1135
+ ...raw,
1136
+ schemaVersion: 2,
1137
+ runtime: {
1138
+ ...raw.runtime,
1139
+ provider: normalizeRuntimeProvider(raw.runtime.provider)
1140
+ },
1141
+ channels: normalizeChannelInstances(rawChannels)
1142
+ };
1143
+ Object.defineProperty(parsed, RAW_CHANNELS, { value: rawChannels, enumerable: false });
1144
+ return parsed;
1145
+ }
1146
+ function atomicWriteFile(filePath, content) {
1147
+ ensureConfigDir();
1148
+ const tmpPath = `${filePath}.${process.pid}.${crypto2.randomUUID()}.tmp`;
907
1149
  try {
908
- const parsed = JSON.parse(fs.readFileSync(CONFIG_V2_PATH, "utf-8"));
909
- if (parsed && parsed.schemaVersion === 2 && parsed.runtime && Array.isArray(parsed.channels)) {
910
- parsed.runtime.provider = normalizeRuntimeProvider(parsed.runtime.provider);
911
- parsed.channels = normalizeChannelInstances(parsed.channels);
912
- return parsed;
1150
+ fs2.writeFileSync(tmpPath, content, { mode: 384 });
1151
+ fs2.renameSync(tmpPath, filePath);
1152
+ } finally {
1153
+ try {
1154
+ fs2.rmSync(tmpPath, { force: true });
1155
+ } catch {
913
1156
  }
914
- return null;
915
- } catch {
916
- return null;
917
1157
  }
918
1158
  }
919
1159
  function writeConfigV2File(config2) {
920
- ensureConfigDir();
921
- const tmpPath = CONFIG_V2_PATH + ".tmp";
922
- fs.writeFileSync(tmpPath, JSON.stringify(config2, null, 2), { mode: 384 });
923
- fs.renameSync(tmpPath, CONFIG_V2_PATH);
1160
+ const serialized = {
1161
+ ...config2,
1162
+ channels: config2[RAW_CHANNELS] || config2.channels
1163
+ };
1164
+ atomicWriteFile(CONFIG_V2_PATH, JSON.stringify(serialized, null, 2));
924
1165
  }
925
1166
  function defaultAliasForProvider(provider) {
926
1167
  return provider === "feishu" ? "\u98DE\u4E66" : "\u5FAE\u4FE1";
@@ -941,6 +1182,7 @@ function normalizeChannelInstances(value) {
941
1182
  const config2 = record.config && typeof record.config === "object" ? record.config : {};
942
1183
  const timestamp = nowIso();
943
1184
  return [{
1185
+ ...record,
944
1186
  id: normalizeChannelId(
945
1187
  typeof record.id === "string" && record.id.trim() ? record.id : buildDefaultChannelId(provider)
946
1188
  ),
@@ -1044,29 +1286,33 @@ function expandConfig(v2) {
1044
1286
  function loadConfig() {
1045
1287
  const current = readConfigV2File();
1046
1288
  if (current) return expandConfig(current);
1047
- const legacyEnv = loadRawConfigEnv();
1048
- if (legacyEnv.size > 0) {
1049
- const migrated = migrateLegacyEnvToV2(legacyEnv);
1050
- writeConfigV2File(migrated);
1051
- return expandConfig(migrated);
1052
- }
1053
- const empty = {
1054
- schemaVersion: 2,
1055
- runtime: {
1056
- provider: "codex",
1057
- defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
1058
- defaultMode: "code",
1059
- historyMessageLimit: 8,
1060
- streamStatusIdleStartSeconds: DEFAULT_STREAM_STATUS_IDLE_START_SECONDS,
1061
- streamStatusCheckIntervalSeconds: DEFAULT_STREAM_STATUS_CHECK_INTERVAL_SECONDS,
1062
- codexSkipGitRepoCheck: true,
1063
- codexSandboxMode: "workspace-write",
1064
- codexReasoningEffort: "medium",
1065
- uiAllowLan: false
1066
- },
1067
- channels: []
1068
- };
1069
- return expandConfig(empty);
1289
+ return withFileLock(CONFIG_V2_PATH, () => {
1290
+ const concurrentlyCreated = readConfigV2File();
1291
+ if (concurrentlyCreated) return expandConfig(concurrentlyCreated);
1292
+ const legacyEnv = loadRawConfigEnv();
1293
+ if (legacyEnv.size > 0) {
1294
+ const migrated = migrateLegacyEnvToV2(legacyEnv);
1295
+ writeConfigV2File(migrated);
1296
+ return expandConfig(migrated);
1297
+ }
1298
+ const empty = {
1299
+ schemaVersion: 2,
1300
+ runtime: {
1301
+ provider: "codex",
1302
+ defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
1303
+ defaultMode: "code",
1304
+ historyMessageLimit: 8,
1305
+ streamStatusIdleStartSeconds: DEFAULT_STREAM_STATUS_IDLE_START_SECONDS,
1306
+ streamStatusCheckIntervalSeconds: DEFAULT_STREAM_STATUS_CHECK_INTERVAL_SECONDS,
1307
+ codexSkipGitRepoCheck: true,
1308
+ codexSandboxMode: "workspace-write",
1309
+ codexReasoningEffort: "medium",
1310
+ uiAllowLan: false
1311
+ },
1312
+ channels: []
1313
+ };
1314
+ return expandConfig(empty);
1315
+ });
1070
1316
  }
1071
1317
  function listChannelInstances(config2) {
1072
1318
  return [...config2?.channels || loadConfig().channels || []];
@@ -1162,53 +1408,6 @@ function configToSettings(config2) {
1162
1408
  return m;
1163
1409
  }
1164
1410
 
1165
- // src/lib/bridge/channel-adapter.ts
1166
- var BaseChannelAdapter = class {
1167
- inboundQueue = [];
1168
- inboundWaiters = [];
1169
- /** Human-readable instance alias, when applicable. */
1170
- alias;
1171
- /**
1172
- * Answer a callback query or interactive-card action when supported.
1173
- * Not all platforms support this — default implementation is a no-op.
1174
- */
1175
- async answerCallback(_callbackQueryId, _text) {
1176
- }
1177
- consumeInboundMessage(isRunning) {
1178
- const queued = this.inboundQueue.shift();
1179
- if (queued) return Promise.resolve(queued);
1180
- if (!isRunning) return Promise.resolve(null);
1181
- return new Promise((resolve2) => {
1182
- this.inboundWaiters.push(resolve2);
1183
- });
1184
- }
1185
- enqueueInboundMessage(message) {
1186
- const waiter = this.inboundWaiters.shift();
1187
- if (waiter) {
1188
- waiter(message);
1189
- } else {
1190
- this.inboundQueue.push(message);
1191
- }
1192
- }
1193
- clearInboundQueue() {
1194
- this.inboundQueue = [];
1195
- }
1196
- rejectPendingInboundConsumers() {
1197
- for (const waiter of this.inboundWaiters) {
1198
- waiter(null);
1199
- }
1200
- this.inboundWaiters = [];
1201
- }
1202
- };
1203
- var adapterFactories = /* @__PURE__ */ new Map();
1204
- function registerAdapterFactory(provider, factory) {
1205
- adapterFactories.set(provider, factory);
1206
- }
1207
- function createAdapter(instance) {
1208
- const factory = adapterFactories.get(instance.provider);
1209
- return factory ? factory(instance) : null;
1210
- }
1211
-
1212
1411
  // src/lib/bridge/markdown/feishu.ts
1213
1412
  function hasComplexMarkdown(text2) {
1214
1413
  if (/```[\s\S]*?```/.test(text2)) return true;
@@ -1431,18 +1630,37 @@ function buildPermissionButtonCard(text2, permissionRequestId, chatId) {
1431
1630
  // src/lib/bridge/adapters/feishu-adapter.ts
1432
1631
  var DEDUP_MAX = 1e3;
1433
1632
  var MAX_FILE_SIZE = 20 * 1024 * 1024;
1633
+ var ATTACHMENT_REQUEST_TIMEOUT_MS = 6e4;
1434
1634
  var COMPLETED_EMOJI = "DONE";
1435
1635
  var ERROR_EMOJI = "ERROR";
1436
1636
  var CARD_TERMINAL_REACTION_DELAY_MS = 2e3;
1437
1637
  var CARD_THROTTLE_MS = 1e3;
1438
1638
  var CARD_REQUEST_TIMEOUT_MS = 15e3;
1439
1639
  var CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS = 1e3;
1640
+ var CARD_FINALIZE_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 3e4];
1641
+ var CARD_STOP_FINALIZE_TIMEOUT_MS = 2e3;
1440
1642
  var CARD_FULL_REFRESH_INTERVAL_MS = 5 * 6e4;
1441
1643
  var FINAL_CARD_FULL_TEXT_MAX_CHARS = 12e3;
1442
1644
  var FINAL_CARD_PREVIEW_CHARS = 4e3;
1443
1645
  var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
1444
1646
  var EMPTY_STREAMING_TASKS = "";
1445
1647
  var EMPTY_STREAMING_TOOLS = "";
1648
+ function validateFeishuAttachmentPath(filePath, maxSize = MAX_FILE_SIZE) {
1649
+ let stat;
1650
+ try {
1651
+ stat = fs3.statSync(filePath);
1652
+ } catch {
1653
+ return `Attachment not found: ${filePath}`;
1654
+ }
1655
+ if (!stat.isFile()) return `Attachment is not a regular file: ${filePath}`;
1656
+ if (stat.size > maxSize) {
1657
+ return `Attachment is too large: ${stat.size} bytes (max ${maxSize} bytes)`;
1658
+ }
1659
+ return null;
1660
+ }
1661
+ function normalizeAttachmentFileName(value) {
1662
+ return value.replace(/[\r\n"]/g, "_") || "attachment.bin";
1663
+ }
1446
1664
  function shouldDeliverFinalTextSeparately(text2) {
1447
1665
  return text2.trim().length > FINAL_CARD_FULL_TEXT_MAX_CHARS;
1448
1666
  }
@@ -1523,12 +1741,18 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1523
1741
  activeCards = /* @__PURE__ */ new Map();
1524
1742
  /** In-flight card creation promises per stream key — prevents duplicate creation. */
1525
1743
  cardCreatePromises = /* @__PURE__ */ new Map();
1744
+ /** Terminal card updates retained until CardKit confirms finalization. */
1745
+ pendingCardFinalizations = /* @__PURE__ */ new Map();
1746
+ cardFinalizePromises = /* @__PURE__ */ new Map();
1526
1747
  /** Cached tenant token for upload APIs. */
1527
1748
  tenantTokenCache = null;
1528
1749
  cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
1529
1750
  cardFinalizeFlushWaitExtraMs = CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS;
1530
1751
  cardFullRefreshIntervalMs = CARD_FULL_REFRESH_INTERVAL_MS;
1531
1752
  cardTerminalReactionDelayMs = CARD_TERMINAL_REACTION_DELAY_MS;
1753
+ cardFinalizeRetryDelaysMs = CARD_FINALIZE_RETRY_DELAYS_MS;
1754
+ cardStopFinalizeTimeoutMs = CARD_STOP_FINALIZE_TIMEOUT_MS;
1755
+ stopping = false;
1532
1756
  constructor(instance) {
1533
1757
  super();
1534
1758
  this.channelType = instance?.id || "feishu";
@@ -1556,6 +1780,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1556
1780
  // ── Lifecycle ───────────────────────────────────────────────
1557
1781
  async start() {
1558
1782
  if (this.running) return;
1783
+ this.stopping = false;
1559
1784
  const configError = this.validateConfig();
1560
1785
  if (configError) {
1561
1786
  console.warn("[feishu-adapter] Cannot start:", configError);
@@ -1609,6 +1834,29 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1609
1834
  async stop() {
1610
1835
  if (!this.running) return;
1611
1836
  this.running = false;
1837
+ this.stopping = true;
1838
+ const finalizations = Array.from(this.activeCards.entries()).map(([cardKey, state]) => {
1839
+ const pending = this.pendingCardFinalizations.get(cardKey);
1840
+ return this.finalizeCard(
1841
+ state.chatId,
1842
+ pending?.status || "interrupted",
1843
+ pending?.responseText || "Bridge \u670D\u52A1\u6B63\u5728\u505C\u6B62\uFF0C\u5F53\u524D\u4EFB\u52A1\u5DF2\u4E2D\u65AD\u3002",
1844
+ cardKey
1845
+ );
1846
+ });
1847
+ if (finalizations.length > 0) {
1848
+ let stopTimeout = null;
1849
+ try {
1850
+ await Promise.race([
1851
+ Promise.allSettled(finalizations),
1852
+ new Promise((resolve2) => {
1853
+ stopTimeout = setTimeout(resolve2, Math.max(0, this.cardStopFinalizeTimeoutMs));
1854
+ })
1855
+ ]);
1856
+ } finally {
1857
+ if (stopTimeout) clearTimeout(stopTimeout);
1858
+ }
1859
+ }
1612
1860
  if (this.wsClient) {
1613
1861
  try {
1614
1862
  this.wsClient.close({ force: true });
@@ -1622,8 +1870,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1622
1870
  for (const [, state] of this.activeCards) {
1623
1871
  if (state.throttleTimer) clearTimeout(state.throttleTimer);
1624
1872
  }
1873
+ for (const request of this.pendingCardFinalizations.values()) {
1874
+ if (request.retryTimer) clearTimeout(request.retryTimer);
1875
+ }
1625
1876
  this.activeCards.clear();
1626
1877
  this.cardCreatePromises.clear();
1878
+ this.pendingCardFinalizations.clear();
1879
+ this.cardFinalizePromises.clear();
1627
1880
  this.seenMessageIds.clear();
1628
1881
  this.lastIncomingMessageId.clear();
1629
1882
  console.log("[feishu-adapter] Stopped");
@@ -1652,6 +1905,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1652
1905
  * Called by bridge-manager via onMessageEnd().
1653
1906
  */
1654
1907
  onMessageEnd(chatId, streamKey) {
1908
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
1909
+ if (this.pendingCardFinalizations.has(cardKey)) return;
1655
1910
  this.cleanupCard(chatId, streamKey);
1656
1911
  }
1657
1912
  // ── Card Action Handler ─────────────────────────────────────
@@ -1955,13 +2210,18 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1955
2210
  const remainingMs = deadline - Date.now();
1956
2211
  if (remainingMs <= 0) return false;
1957
2212
  const timedOut = Symbol("flush-timeout");
2213
+ let flushTimeout = null;
1958
2214
  try {
1959
2215
  const result = await Promise.race([
1960
2216
  inFlight.then(() => null),
1961
- new Promise((resolve2) => setTimeout(() => resolve2(timedOut), remainingMs))
2217
+ new Promise((resolve2) => {
2218
+ flushTimeout = setTimeout(() => resolve2(timedOut), remainingMs);
2219
+ })
1962
2220
  ]);
1963
2221
  if (result === timedOut) return false;
1964
2222
  } catch {
2223
+ } finally {
2224
+ if (flushTimeout) clearTimeout(flushTimeout);
1965
2225
  }
1966
2226
  continue;
1967
2227
  }
@@ -1986,10 +2246,60 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1986
2246
  } catch {
1987
2247
  }
1988
2248
  }
2249
+ const existingRequest = this.pendingCardFinalizations.get(cardKey);
2250
+ if (existingRequest?.retryTimer) {
2251
+ clearTimeout(existingRequest.retryTimer);
2252
+ }
2253
+ const request = existingRequest || {
2254
+ chatId,
2255
+ status,
2256
+ responseText,
2257
+ streamKey,
2258
+ retryIndex: 0,
2259
+ retryTimer: null
2260
+ };
2261
+ request.chatId = chatId;
2262
+ request.status = status;
2263
+ request.responseText = responseText;
2264
+ request.streamKey = streamKey;
2265
+ request.retryTimer = null;
2266
+ this.pendingCardFinalizations.set(cardKey, request);
2267
+ const result = await this.runCardFinalizationAttempt(cardKey);
2268
+ if (!result.finalized) {
2269
+ if (result.retryable) {
2270
+ this.scheduleCardFinalizationRetry(cardKey);
2271
+ } else {
2272
+ this.clearPendingCardFinalization(cardKey);
2273
+ }
2274
+ }
2275
+ return result.finalized && result.textCovered;
2276
+ }
2277
+ async runCardFinalizationAttempt(cardKey) {
2278
+ const existing = this.cardFinalizePromises.get(cardKey);
2279
+ if (existing) return existing;
2280
+ const attempt = this.performCardFinalizationAttempt(cardKey);
2281
+ this.cardFinalizePromises.set(cardKey, attempt);
2282
+ try {
2283
+ return await attempt;
2284
+ } finally {
2285
+ if (this.cardFinalizePromises.get(cardKey) === attempt) {
2286
+ this.cardFinalizePromises.delete(cardKey);
2287
+ }
2288
+ }
2289
+ }
2290
+ async performCardFinalizationAttempt(cardKey) {
2291
+ const request = this.pendingCardFinalizations.get(cardKey);
1989
2292
  const state = this.activeCards.get(cardKey);
1990
- if (!state || !this.restClient) return false;
2293
+ if (!request || !state) {
2294
+ return { finalized: false, textCovered: false, retryable: false };
2295
+ }
2296
+ if (!this.restClient) {
2297
+ return { finalized: false, textCovered: false, retryable: !this.stopping };
2298
+ }
1991
2299
  const cardkit = this.restClient.cardkit?.v1;
1992
- if (!cardkit?.card?.settings || !cardkit?.card?.update) return false;
2300
+ if (!cardkit?.card?.settings || !cardkit?.card?.update) {
2301
+ return { finalized: false, textCovered: false, retryable: false };
2302
+ }
1993
2303
  if (state.throttleTimer) {
1994
2304
  clearTimeout(state.throttleTimer);
1995
2305
  state.throttleTimer = null;
@@ -2016,21 +2326,21 @@ var FeishuAdapter = class extends BaseChannelAdapter {
2016
2326
  };
2017
2327
  const elapsedMs = Date.now() - state.startTime;
2018
2328
  const footer = {
2019
- status: statusLabels[status] || status,
2329
+ status: statusLabels[request.status] || request.status,
2020
2330
  elapsed: formatElapsed(elapsedMs)
2021
2331
  };
2022
2332
  const existingText = state.pendingText || "";
2023
2333
  const trimmedExisting = existingText.trim();
2024
- const trimmedResponse = responseText.trim();
2334
+ const trimmedResponse = request.responseText.trim();
2025
2335
  let finalText = trimmedResponse || trimmedExisting;
2026
- if (status === "interrupted" && trimmedExisting && trimmedResponse && trimmedResponse !== trimmedExisting && !trimmedExisting.includes(trimmedResponse)) {
2336
+ if (request.status === "interrupted" && trimmedExisting && trimmedResponse && trimmedResponse !== trimmedExisting && !trimmedExisting.includes(trimmedResponse)) {
2027
2337
  finalText = `${trimmedExisting}
2028
2338
 
2029
2339
  ${trimmedResponse}`;
2030
2340
  }
2031
2341
  const deliverTextSeparately = shouldDeliverFinalTextSeparately(finalText);
2032
2342
  const cardText = deliverTextSeparately ? buildFinalCardTextPreview(finalText) : finalText;
2033
- const finalCardJson = buildFinalCardJson(cardText, state.taskItems, state.toolCalls, footer, status);
2343
+ const finalCardJson = buildFinalCardJson(cardText, state.taskItems, state.toolCalls, footer, request.status);
2034
2344
  state.sequence++;
2035
2345
  await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
2036
2346
  path: { card_id: state.cardId },
@@ -2040,20 +2350,52 @@ ${trimmedResponse}`;
2040
2350
  }
2041
2351
  }));
2042
2352
  this.activeCards.delete(cardKey);
2043
- console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${status}, elapsed=${formatElapsed(elapsedMs)}`);
2044
- const terminalReactionEmoji = status === "completed" ? COMPLETED_EMOJI : status === "error" ? ERROR_EMOJI : null;
2353
+ this.clearPendingCardFinalization(cardKey);
2354
+ console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${request.status}, elapsed=${formatElapsed(elapsedMs)}`);
2355
+ const terminalReactionEmoji = request.status === "completed" ? COMPLETED_EMOJI : request.status === "error" ? ERROR_EMOJI : null;
2045
2356
  if (terminalReactionEmoji && this.hasTerminalReactionApi()) {
2046
2357
  await this.waitBeforeTerminalReaction();
2047
2358
  await this.addTerminalReaction(cardKey, state.messageId, terminalReactionEmoji);
2048
2359
  }
2049
- return !deliverTextSeparately;
2360
+ return { finalized: true, textCovered: !deliverTextSeparately, retryable: false };
2050
2361
  } catch (err) {
2051
2362
  console.warn("[feishu-adapter] Card finalize failed:", err instanceof Error ? err.message : err);
2052
- return false;
2053
- } finally {
2054
- this.activeCards.delete(cardKey);
2363
+ return { finalized: false, textCovered: false, retryable: true };
2055
2364
  }
2056
2365
  }
2366
+ scheduleCardFinalizationRetry(cardKey) {
2367
+ const request = this.pendingCardFinalizations.get(cardKey);
2368
+ if (!request || request.retryTimer || this.stopping || !this.restClient) return;
2369
+ if (request.retryIndex >= this.cardFinalizeRetryDelaysMs.length) {
2370
+ console.warn(`[feishu-adapter] Card finalize retries exhausted: streamKey=${cardKey}`);
2371
+ this.cleanupCard(request.chatId, request.streamKey || cardKey);
2372
+ return;
2373
+ }
2374
+ const delayMs = Math.max(0, this.cardFinalizeRetryDelaysMs[request.retryIndex] || 0);
2375
+ request.retryIndex += 1;
2376
+ request.retryTimer = setTimeout(() => {
2377
+ const current = this.pendingCardFinalizations.get(cardKey);
2378
+ if (!current) return;
2379
+ current.retryTimer = null;
2380
+ void this.runCardFinalizationAttempt(cardKey).then((result) => {
2381
+ if (result.finalized) return;
2382
+ if (result.retryable) {
2383
+ this.scheduleCardFinalizationRetry(cardKey);
2384
+ } else {
2385
+ this.cleanupCard(current.chatId, current.streamKey || cardKey);
2386
+ }
2387
+ }).catch((error) => {
2388
+ console.warn("[feishu-adapter] Card finalize retry failed:", error instanceof Error ? error.message : error);
2389
+ this.scheduleCardFinalizationRetry(cardKey);
2390
+ });
2391
+ }, delayMs);
2392
+ request.retryTimer.unref?.();
2393
+ }
2394
+ clearPendingCardFinalization(cardKey) {
2395
+ const request = this.pendingCardFinalizations.get(cardKey);
2396
+ if (request?.retryTimer) clearTimeout(request.retryTimer);
2397
+ this.pendingCardFinalizations.delete(cardKey);
2398
+ }
2057
2399
  hasTerminalReactionApi() {
2058
2400
  const messageReaction = this.restClient?.im?.messageReaction;
2059
2401
  return typeof messageReaction?.create === "function";
@@ -2084,6 +2426,7 @@ ${trimmedResponse}`;
2084
2426
  cleanupCard(chatId, streamKey) {
2085
2427
  const cardKey = this.resolveStreamKey(chatId, streamKey);
2086
2428
  this.cardCreatePromises.delete(cardKey);
2429
+ this.clearPendingCardFinalization(cardKey);
2087
2430
  const state = this.activeCards.get(cardKey);
2088
2431
  if (!state) return;
2089
2432
  if (state.throttleTimer) {
@@ -2322,7 +2665,8 @@ ${trimmedResponse}`;
2322
2665
  body: JSON.stringify({
2323
2666
  app_id: appId,
2324
2667
  app_secret: appSecret
2325
- })
2668
+ }),
2669
+ signal: AbortSignal.timeout(CARD_REQUEST_TIMEOUT_MS)
2326
2670
  });
2327
2671
  const data = await response.json();
2328
2672
  if (!response.ok || data.code !== 0 || !data.tenant_access_token) {
@@ -2347,9 +2691,8 @@ ${trimmedResponse}`;
2347
2691
  return { ok: true, messageId: lastMessageId };
2348
2692
  }
2349
2693
  async sendAttachment(chatId, attachment, replyToMessageId) {
2350
- if (!fs2.existsSync(attachment.path)) {
2351
- return { ok: false, error: `Attachment not found: ${attachment.path}` };
2352
- }
2694
+ const validationError = validateFeishuAttachmentPath(attachment.path);
2695
+ if (validationError) return { ok: false, error: validationError };
2353
2696
  try {
2354
2697
  if (attachment.kind === "image") {
2355
2698
  const imageKey = await this.uploadImage(attachment);
@@ -2373,14 +2716,17 @@ ${trimmedResponse}`;
2373
2716
  }
2374
2717
  async uploadImage(attachment) {
2375
2718
  const token = await this.getTenantAccessToken();
2376
- const fileName = attachment.name || path2.basename(attachment.path) || "image.png";
2719
+ const fileName = normalizeAttachmentFileName(
2720
+ attachment.name || path2.basename(attachment.path) || "image.png"
2721
+ );
2377
2722
  const form = new FormData();
2378
2723
  form.set("image_type", "message");
2379
- form.set("image", new Blob([fs2.readFileSync(attachment.path)]), fileName);
2724
+ form.set("image", new Blob([await fs3.promises.readFile(attachment.path)]), fileName);
2380
2725
  const response = await fetch(`${this.getOpenApiBaseUrl()}/open-apis/im/v1/images`, {
2381
2726
  method: "POST",
2382
2727
  headers: { Authorization: `Bearer ${token}` },
2383
- body: form
2728
+ body: form,
2729
+ signal: AbortSignal.timeout(ATTACHMENT_REQUEST_TIMEOUT_MS)
2384
2730
  });
2385
2731
  const data = await response.json();
2386
2732
  if (!response.ok || data.code !== 0 || !data.data?.image_key) {
@@ -2390,15 +2736,18 @@ ${trimmedResponse}`;
2390
2736
  }
2391
2737
  async uploadFile(attachment) {
2392
2738
  const token = await this.getTenantAccessToken();
2393
- const fileName = attachment.name || path2.basename(attachment.path) || "attachment.bin";
2739
+ const fileName = normalizeAttachmentFileName(
2740
+ attachment.name || path2.basename(attachment.path) || "attachment.bin"
2741
+ );
2394
2742
  const form = new FormData();
2395
2743
  form.set("file_type", "stream");
2396
2744
  form.set("file_name", fileName);
2397
- form.set("file", new Blob([fs2.readFileSync(attachment.path)]), fileName);
2745
+ form.set("file", new Blob([await fs3.promises.readFile(attachment.path)]), fileName);
2398
2746
  const response = await fetch(`${this.getOpenApiBaseUrl()}/open-apis/im/v1/files`, {
2399
2747
  method: "POST",
2400
2748
  headers: { Authorization: `Bearer ${token}` },
2401
- body: form
2749
+ body: form,
2750
+ signal: AbortSignal.timeout(ATTACHMENT_REQUEST_TIMEOUT_MS)
2402
2751
  });
2403
2752
  const data = await response.json();
2404
2753
  if (!response.ok || data.code !== 0 || !data.data?.file_key) {
@@ -2937,20 +3286,20 @@ ${trimmedResponse}`;
2937
3286
  buffer = Buffer.concat(chunks);
2938
3287
  } catch (streamErr) {
2939
3288
  console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
2940
- const fs16 = await import("fs");
3289
+ const fs17 = await import("fs");
2941
3290
  const os5 = await import("os");
2942
3291
  const path19 = await import("path");
2943
- const tmpPath = path19.join(os5.tmpdir(), `feishu-dl-${crypto.randomUUID()}`);
3292
+ const tmpPath = path19.join(os5.tmpdir(), `feishu-dl-${crypto3.randomUUID()}`);
2944
3293
  try {
2945
3294
  await res.writeFile(tmpPath);
2946
- buffer = fs16.readFileSync(tmpPath);
3295
+ buffer = fs17.readFileSync(tmpPath);
2947
3296
  if (buffer.length > MAX_FILE_SIZE) {
2948
3297
  console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
2949
3298
  return null;
2950
3299
  }
2951
3300
  } finally {
2952
3301
  try {
2953
- fs16.unlinkSync(tmpPath);
3302
+ fs17.unlinkSync(tmpPath);
2954
3303
  } catch {
2955
3304
  }
2956
3305
  }
@@ -2960,7 +3309,7 @@ ${trimmedResponse}`;
2960
3309
  return null;
2961
3310
  }
2962
3311
  const base64 = buffer.toString("base64");
2963
- const id = crypto.randomUUID();
3312
+ const id = crypto3.randomUUID();
2964
3313
  const mimeType = MIME_BY_TYPE[resourceType] || "application/octet-stream";
2965
3314
  const ext = resourceType === "image" ? "png" : resourceType === "audio" ? "ogg" : resourceType === "video" ? "mp4" : "bin";
2966
3315
  console.log(`[feishu-adapter] Resource downloaded: ${buffer.length} bytes, key=${fileKey}`);
@@ -2996,25 +3345,34 @@ ${trimmedResponse}`;
2996
3345
  registerAdapterFactory("feishu", (instance) => new FeishuAdapter(instance));
2997
3346
 
2998
3347
  // src/weixin-store.ts
2999
- import fs3 from "node:fs";
3348
+ import fs4 from "node:fs";
3349
+ import crypto4 from "node:crypto";
3000
3350
  import path3 from "node:path";
3001
3351
  var DATA_DIR = path3.join(CTI_HOME, "data");
3002
3352
  var ACCOUNTS_PATH = path3.join(DATA_DIR, "weixin-accounts.json");
3003
3353
  var CONTEXT_TOKENS_PATH = path3.join(DATA_DIR, "weixin-context-tokens.json");
3004
3354
  function ensureDir(dir) {
3005
- fs3.mkdirSync(dir, { recursive: true });
3355
+ fs4.mkdirSync(dir, { recursive: true });
3006
3356
  }
3007
3357
  function atomicWrite(filePath, data) {
3008
- const tmpPath = `${filePath}.tmp`;
3009
- fs3.writeFileSync(tmpPath, data, "utf-8");
3010
- fs3.renameSync(tmpPath, filePath);
3358
+ const tmpPath = `${filePath}.${process.pid}.${crypto4.randomUUID()}.tmp`;
3359
+ try {
3360
+ fs4.writeFileSync(tmpPath, data, "utf-8");
3361
+ fs4.renameSync(tmpPath, filePath);
3362
+ } finally {
3363
+ try {
3364
+ fs4.rmSync(tmpPath, { force: true });
3365
+ } catch {
3366
+ }
3367
+ }
3011
3368
  }
3012
3369
  function readJson(filePath, fallback) {
3013
3370
  try {
3014
- const raw = fs3.readFileSync(filePath, "utf-8");
3371
+ const raw = fs4.readFileSync(filePath, "utf-8");
3015
3372
  return JSON.parse(raw);
3016
- } catch {
3017
- return fallback;
3373
+ } catch (error) {
3374
+ if (error.code === "ENOENT") return fallback;
3375
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u5FAE\u4FE1\u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
3018
3376
  }
3019
3377
  }
3020
3378
  function getAccountRecency(account) {
@@ -3069,13 +3427,15 @@ function getWeixinContextToken(accountId, peerUserId) {
3069
3427
  return tokens[contextKey(accountId, peerUserId)];
3070
3428
  }
3071
3429
  function upsertWeixinContextToken(accountId, peerUserId, contextToken) {
3072
- const tokens = readContextTokens();
3073
- tokens[contextKey(accountId, peerUserId)] = contextToken;
3074
- writeContextTokens(tokens);
3430
+ withFileLock(CONTEXT_TOKENS_PATH, () => {
3431
+ const tokens = readContextTokens();
3432
+ tokens[contextKey(accountId, peerUserId)] = contextToken;
3433
+ writeContextTokens(tokens);
3434
+ });
3075
3435
  }
3076
3436
 
3077
3437
  // src/adapters/weixin/weixin-api.ts
3078
- import crypto2 from "node:crypto";
3438
+ import crypto5 from "node:crypto";
3079
3439
 
3080
3440
  // src/adapters/weixin/weixin-types.ts
3081
3441
  var MessageItemType = {
@@ -3099,7 +3459,7 @@ var LONG_POLL_TIMEOUT_MS = 35e3;
3099
3459
  var API_TIMEOUT_MS = 15e3;
3100
3460
  var CONFIG_TIMEOUT_MS = 1e4;
3101
3461
  function generateWechatUin() {
3102
- return crypto2.randomBytes(4).toString("base64");
3462
+ return crypto5.randomBytes(4).toString("base64");
3103
3463
  }
3104
3464
  function normalizeBaseUrl(baseUrl) {
3105
3465
  return (baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -3158,7 +3518,7 @@ async function getUpdates(creds, getUpdatesBuf, timeoutMs = LONG_POLL_TIMEOUT_MS
3158
3518
  }
3159
3519
  }
3160
3520
  function generateClientId() {
3161
- return `cti-weixin-${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
3521
+ return `cti-weixin-${Date.now()}-${crypto5.randomBytes(4).toString("hex")}`;
3162
3522
  }
3163
3523
  async function sendMessage(creds, toUserId, items, contextToken) {
3164
3524
  const clientId = generateClientId();
@@ -3235,10 +3595,35 @@ function decodeWeixinChatId(chatId) {
3235
3595
  }
3236
3596
 
3237
3597
  // src/adapters/weixin/weixin-media.ts
3238
- import crypto3 from "node:crypto";
3598
+ import crypto6 from "node:crypto";
3239
3599
  var MAX_MEDIA_SIZE = 100 * 1024 * 1024;
3600
+ async function readResponseBodyWithLimit(response, maxSize, label) {
3601
+ const contentLength = Number(response.headers.get("content-length"));
3602
+ if (Number.isFinite(contentLength) && contentLength > maxSize) {
3603
+ throw new Error(`Media too large: ${contentLength} bytes`);
3604
+ }
3605
+ if (!response.body) return Buffer.alloc(0);
3606
+ const reader = response.body.getReader();
3607
+ const chunks = [];
3608
+ let totalSize = 0;
3609
+ try {
3610
+ while (true) {
3611
+ const { done, value } = await reader.read();
3612
+ if (done) break;
3613
+ totalSize += value.byteLength;
3614
+ if (totalSize > maxSize) {
3615
+ await reader.cancel(`Media too large while downloading ${label}`);
3616
+ throw new Error(`Media too large: more than ${maxSize} bytes`);
3617
+ }
3618
+ chunks.push(Buffer.from(value));
3619
+ }
3620
+ } finally {
3621
+ reader.releaseLock();
3622
+ }
3623
+ return Buffer.concat(chunks, totalSize);
3624
+ }
3240
3625
  function decryptMedia(data, key) {
3241
- const decipher = crypto3.createDecipheriv("aes-128-ecb", key, null);
3626
+ const decipher = crypto6.createDecipheriv("aes-128-ecb", key, null);
3242
3627
  return Buffer.concat([decipher.update(data), decipher.final()]);
3243
3628
  }
3244
3629
  function parseBase64EncodedAesKey(aesKeyBase64) {
@@ -3293,13 +3678,10 @@ async function downloadAndDecryptMedia(cdnUrl, aesKey, label) {
3293
3678
  if (!res.ok) {
3294
3679
  throw new Error(`CDN download failed for ${label}: ${res.status}`);
3295
3680
  }
3296
- const encrypted = Buffer.from(await res.arrayBuffer());
3681
+ const encrypted = await readResponseBodyWithLimit(res, MAX_MEDIA_SIZE, label);
3297
3682
  if (encrypted.length === 0) {
3298
3683
  throw new Error(`Downloaded ${label} is empty`);
3299
3684
  }
3300
- if (encrypted.length > MAX_MEDIA_SIZE) {
3301
- throw new Error(`Media too large: ${encrypted.length} bytes`);
3302
- }
3303
3685
  return decryptMedia(encrypted, aesKey);
3304
3686
  }
3305
3687
  async function downloadMediaFromItem(item, cdnBaseUrl) {
@@ -3348,7 +3730,7 @@ async function downloadMediaFromItem(item, cdnBaseUrl) {
3348
3730
  }
3349
3731
  const data = await downloadAndDecryptMedia(buildCdnDownloadUrl(encryptParam, cdnBaseUrl), aesKey, filename);
3350
3732
  return {
3351
- id: crypto3.randomUUID(),
3733
+ id: crypto6.randomUUID(),
3352
3734
  name: filename,
3353
3735
  type: mimeType,
3354
3736
  size: data.length,
@@ -7080,6 +7462,16 @@ function escape2(state, silent) {
7080
7462
  state.pos = pos;
7081
7463
  return true;
7082
7464
  }
7465
+ if (ch1 === 32) {
7466
+ if (!silent) {
7467
+ const token = state.push("text_special", "", 0);
7468
+ token.content = "\\";
7469
+ token.markup = "\\";
7470
+ token.info = "escape";
7471
+ }
7472
+ state.pos = pos;
7473
+ return true;
7474
+ }
7083
7475
  let escapedStr = state.src[pos];
7084
7476
  if (ch1 >= 55296 && ch1 <= 56319 && pos + 1 < max) {
7085
7477
  const ch2 = state.src.charCodeAt(pos + 1);
@@ -7895,34 +8287,34 @@ function re_default(opts) {
7895
8287
  re.src_ZPCc = [re.src_Z, re.src_P, re.src_Cc].join("|");
7896
8288
  re.src_ZCc = [re.src_Z, re.src_Cc].join("|");
7897
8289
  const text_separators = "[><\uFF5C]";
7898
- re.src_pseudo_letter = "(?:(?!" + text_separators + "|" + re.src_ZPCc + ")" + re.src_Any + ")";
8290
+ re.src_pseudo_letter = `(?:(?!${text_separators}|${re.src_ZPCc})${re.src_Any})`;
7899
8291
  re.src_ip4 = "(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
7900
- re.src_auth = "(?:(?:(?!" + re.src_ZCc + "|[@/\\[\\]()]).)+@)?";
8292
+ re.src_auth = `(?:(?:(?!${re.src_ZCc}|[@/\\[\\]()]).){1,50}@)?`;
7901
8293
  re.src_port = "(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?";
7902
- re.src_host_terminator = "(?=$|" + text_separators + "|" + re.src_ZPCc + ")(?!" + (opts["---"] ? "-(?!--)|" : "-|") + "_|:\\d|\\.-|\\.(?!$|" + re.src_ZPCc + "))";
7903
- re.src_path = "(?:[/?#](?:(?!" + re.src_ZCc + "|" + text_separators + `|[()[\\]{}.,"'?!\\-;]).|\\[(?:(?!` + re.src_ZCc + "|\\]).)*\\]|\\((?:(?!" + re.src_ZCc + "|[)]).)*\\)|\\{(?:(?!" + re.src_ZCc + '|[}]).)*\\}|\\"(?:(?!' + re.src_ZCc + `|["]).)+\\"|\\'(?:(?!` + re.src_ZCc + "|[']).)+\\'|\\'(?=" + re.src_pseudo_letter + "|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!" + re.src_ZCc + "|[.]|$)|" + (opts["---"] ? "\\-(?!--(?:[^-]|$))(?:-*)|" : "\\-+|") + // allow `,,,` in paths
7904
- ",(?!" + re.src_ZCc + "|$)|;(?!" + re.src_ZCc + "|$)|\\!+(?!" + re.src_ZCc + "|[!]|$)|\\?(?!" + re.src_ZCc + "|[?]|$))+|\\/)?";
7905
- re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*';
8294
+ re.src_host_terminator = `(?=$|${text_separators}|${re.src_ZPCc})(?!${opts["---"] ? "-(?!--)|" : "-|"}_|:\\d|\\.-|\\.(?!$|${re.src_ZPCc}))`;
8295
+ re.src_path = `(?:[/?#](?:(?!${re.src_ZCc}|${text_separators}|[()[\\]{}.,"'?!\\-;]).|\\[(?:(?!${re.src_ZCc}|\\]).)*\\]|\\((?:(?!${re.src_ZCc}|[)]).)*\\)|\\{(?:(?!${re.src_ZCc}|[}]).)*\\}|\\"(?:(?!${re.src_ZCc}|["]).)+\\"|\\'(?:(?!${re.src_ZCc}|[']).)+\\'|\\'(?=${re.src_pseudo_letter}|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!${re.src_ZCc}|[.]|$)|` + (opts["---"] ? "\\-(?!--(?:[^-]|$))(?:-*)|" : "\\-+|") + // allow `,,,` in paths
8296
+ `,(?!${re.src_ZCc}|$)|;(?!${re.src_ZCc}|$)|\\!+(?!${re.src_ZCc}|[!]|$)|\\?(?!${re.src_ZCc}|[?]|$))+|\\/)?`;
8297
+ re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]{0,63}';
7906
8298
  re.src_xn = "xn--[a-z0-9\\-]{1,59}";
7907
8299
  re.src_domain_root = // Allow letters & digits (http://test1)
7908
- "(?:" + re.src_xn + "|" + re.src_pseudo_letter + "{1,63})";
7909
- re.src_domain = "(?:" + re.src_xn + "|(?:" + re.src_pseudo_letter + ")|(?:" + re.src_pseudo_letter + "(?:-|" + re.src_pseudo_letter + "){0,61}" + re.src_pseudo_letter + "))";
7910
- re.src_host = "(?:(?:(?:(?:" + re.src_domain + ")\\.)*" + re.src_domain + "))";
7911
- re.tpl_host_fuzzy = "(?:" + re.src_ip4 + "|(?:(?:(?:" + re.src_domain + ")\\.)+(?:%TLDS%)))";
7912
- re.tpl_host_no_ip_fuzzy = "(?:(?:(?:" + re.src_domain + ")\\.)+(?:%TLDS%))";
8300
+ "(?:" + re.src_xn + `|${re.src_pseudo_letter}{1,63})`;
8301
+ re.src_domain = "(?:" + re.src_xn + `|(?:${re.src_pseudo_letter})|(?:${re.src_pseudo_letter}(?:-|${re.src_pseudo_letter}){0,61}${re.src_pseudo_letter}))`;
8302
+ re.src_host = `(?:(?:(?:(?:${re.src_domain})\\.)*${re.src_domain}))`;
8303
+ re.tpl_host_fuzzy = "(?:" + re.src_ip4 + `|(?:(?:(?:${re.src_domain})\\.)+(?:%TLDS%)))`;
8304
+ re.tpl_host_no_ip_fuzzy = `(?:(?:(?:${re.src_domain})\\.)+(?:%TLDS%))`;
7913
8305
  re.src_host_strict = re.src_host + re.src_host_terminator;
7914
8306
  re.tpl_host_fuzzy_strict = re.tpl_host_fuzzy + re.src_host_terminator;
7915
8307
  re.src_host_port_strict = re.src_host + re.src_port + re.src_host_terminator;
7916
8308
  re.tpl_host_port_fuzzy_strict = re.tpl_host_fuzzy + re.src_port + re.src_host_terminator;
7917
8309
  re.tpl_host_port_no_ip_fuzzy_strict = re.tpl_host_no_ip_fuzzy + re.src_port + re.src_host_terminator;
7918
- re.tpl_host_fuzzy_test = "localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:" + re.src_ZPCc + "|>|$))";
7919
- re.tpl_email_fuzzy = "(^|" + text_separators + '|"|\\(|' + re.src_ZCc + ")(" + re.src_email_name + "@" + re.tpl_host_fuzzy_strict + ")";
8310
+ re.tpl_host_fuzzy_test = `localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:${re.src_ZPCc}|>|$))`;
8311
+ re.tpl_email_fuzzy = `(^|${text_separators}|"|\\(|${re.src_ZCc})(${re.src_email_name}@${re.tpl_host_fuzzy_strict})`;
7920
8312
  re.tpl_link_fuzzy = // Fuzzy link can't be prepended with .:/\- and non punctuation.
7921
8313
  // but can start with > (markdown blockquote)
7922
- "(^|(?![.:/\\-_@])(?:[$+<=>^`|\uFF5C]|" + re.src_ZPCc + "))((?![$+<=>^`|\uFF5C])" + re.tpl_host_port_fuzzy_strict + re.src_path + ")";
8314
+ `(^|(?![.:/\\-_@])(?:[$+<=>^\`|\uFF5C]|${re.src_ZPCc}))((?![$+<=>^\`|\uFF5C])${re.tpl_host_port_fuzzy_strict}${re.src_path})`;
7923
8315
  re.tpl_link_no_ip_fuzzy = // Fuzzy link can't be prepended with .:/\- and non punctuation.
7924
8316
  // but can start with > (markdown blockquote)
7925
- "(^|(?![.:/\\-_@])(?:[$+<=>^`|\uFF5C]|" + re.src_ZPCc + "))((?![$+<=>^`|\uFF5C])" + re.tpl_host_port_no_ip_fuzzy_strict + re.src_path + ")";
8317
+ `(^|(?![.:/\\-_@])(?:[$+<=>^\`|\uFF5C]|${re.src_ZPCc}))((?![$+<=>^\`|\uFF5C])${re.tpl_host_port_no_ip_fuzzy_strict}${re.src_path})`;
7926
8318
  return re;
7927
8319
  }
7928
8320
 
@@ -7973,7 +8365,7 @@ var defaultSchemas = {
7973
8365
  const tail = text2.slice(pos);
7974
8366
  if (!self.re.http) {
7975
8367
  self.re.http = new RegExp(
7976
- "^\\/\\/" + self.re.src_auth + self.re.src_host_port_strict + self.re.src_path,
8368
+ `^\\/\\/${self.re.src_auth}${self.re.src_host_port_strict}${self.re.src_path}`,
7977
8369
  "i"
7978
8370
  );
7979
8371
  }
@@ -7992,7 +8384,7 @@ var defaultSchemas = {
7992
8384
  self.re.no_http = new RegExp(
7993
8385
  "^" + self.re.src_auth + // Don't allow single-level domains, because of false positives like '//test'
7994
8386
  // with code comments
7995
- "(?:localhost|(?:(?:" + self.re.src_domain + ")\\.)+" + self.re.src_domain_root + ")" + self.re.src_port + self.re.src_host_terminator + self.re.src_path,
8387
+ `(?:localhost|(?:(?:${self.re.src_domain})\\.)+${self.re.src_domain_root})` + self.re.src_port + self.re.src_host_terminator + self.re.src_path,
7996
8388
  "i"
7997
8389
  );
7998
8390
  }
@@ -8013,7 +8405,7 @@ var defaultSchemas = {
8013
8405
  const tail = text2.slice(pos);
8014
8406
  if (!self.re.mailto) {
8015
8407
  self.re.mailto = new RegExp(
8016
- "^" + self.re.src_email_name + "@" + self.re.src_host_strict,
8408
+ `^${self.re.src_email_name}@${self.re.src_host_strict}`,
8017
8409
  "i"
8018
8410
  );
8019
8411
  }
@@ -8062,7 +8454,7 @@ function compile(self) {
8062
8454
  const aliases = [];
8063
8455
  self.__compiled__ = {};
8064
8456
  function schemaError(name, val) {
8065
- throw new Error('(LinkifyIt) Invalid schema "' + name + '": ' + val);
8457
+ throw new Error(`(LinkifyIt) Invalid schema "${name}": ${val}`);
8066
8458
  }
8067
8459
  Object.keys(self.__schemas__).forEach(function(name) {
8068
8460
  const val = self.__schemas__[name];
@@ -8105,11 +8497,11 @@ function compile(self) {
8105
8497
  const slist = Object.keys(self.__compiled__).filter(function(name) {
8106
8498
  return name.length > 0 && self.__compiled__[name];
8107
8499
  }).map(escapeRE2).join("|");
8108
- self.re.schema_test = RegExp("(^|(?!_)(?:[><\uFF5C]|" + re.src_ZPCc + "))(" + slist + ")", "i");
8109
- self.re.schema_search = RegExp("(^|(?!_)(?:[><\uFF5C]|" + re.src_ZPCc + "))(" + slist + ")", "ig");
8110
- self.re.schema_at_start = RegExp("^" + self.re.schema_search.source, "i");
8500
+ self.re.schema_test = RegExp(`(^|(?!_)(?:[><\uFF5C]|${re.src_ZPCc}))(${slist})`, "i");
8501
+ self.re.schema_search = RegExp(`(^|(?!_)(?:[><\uFF5C]|${re.src_ZPCc}))(${slist})`, "ig");
8502
+ self.re.schema_at_start = RegExp(`^${self.re.schema_search.source}`, "i");
8111
8503
  self.re.pretest = RegExp(
8112
- "(" + self.re.schema_test.source + ")|(" + self.re.host_fuzzy_test.source + ")|@",
8504
+ `(${self.re.schema_test.source})|(${self.re.host_fuzzy_test.source})|@`,
8113
8505
  "i"
8114
8506
  );
8115
8507
  }
@@ -8303,10 +8695,10 @@ LinkifyIt.prototype.tlds = function tlds(list2, keepOld) {
8303
8695
  };
8304
8696
  LinkifyIt.prototype.normalize = function normalize2(match2) {
8305
8697
  if (!match2.schema) {
8306
- match2.url = "http://" + match2.url;
8698
+ match2.url = `http://${match2.url}`;
8307
8699
  }
8308
8700
  if (match2.schema === "mailto:" && !/^mailto:/i.test(match2.url)) {
8309
- match2.url = "mailto:" + match2.url;
8701
+ match2.url = `mailto:${match2.url}`;
8310
8702
  }
8311
8703
  };
8312
8704
  LinkifyIt.prototype.onCompile = function onCompile() {
@@ -9352,6 +9744,9 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9352
9744
  consecutiveFailures = /* @__PURE__ */ new Map();
9353
9745
  typingTickets = /* @__PURE__ */ new Map();
9354
9746
  pendingCursors = /* @__PURE__ */ new Map();
9747
+ pollCursors = /* @__PURE__ */ new Map();
9748
+ cursorGenerations = /* @__PURE__ */ new Map();
9749
+ cursorRecoveryGates = /* @__PURE__ */ new Map();
9355
9750
  nextBatchId = 1;
9356
9751
  constructor(instance) {
9357
9752
  super();
@@ -9392,6 +9787,10 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9392
9787
  this.stopAccountWorker(accountId);
9393
9788
  }
9394
9789
  this.pendingCursors.clear();
9790
+ this.pollCursors.clear();
9791
+ this.cursorGenerations.clear();
9792
+ for (const gate of this.cursorRecoveryGates.values()) gate.resolve();
9793
+ this.cursorRecoveryGates.clear();
9395
9794
  this.clearInboundQueue();
9396
9795
  this.rejectPendingInboundConsumers();
9397
9796
  console.log("[weixin-adapter] Stopped");
@@ -9451,11 +9850,11 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9451
9850
  isAuthorized(_userId, _chatId) {
9452
9851
  return true;
9453
9852
  }
9454
- acknowledgeUpdate(updateId) {
9455
- const batch = this.pendingCursors.get(updateId);
9456
- if (!batch) return;
9457
- batch.remaining = Math.max(0, batch.remaining - 1);
9458
- this.maybeCommitPendingCursor(updateId);
9853
+ acknowledgeUpdate(updateId, messageId) {
9854
+ this.settlePendingCursorMessage(updateId, messageId, false);
9855
+ }
9856
+ rejectUpdate(updateId, messageId) {
9857
+ this.settlePendingCursorMessage(updateId, messageId, true);
9459
9858
  }
9460
9859
  onMessageStart(chatId) {
9461
9860
  this.sendTypingIndicator(chatId, TypingStatus.TYPING).catch(() => {
@@ -9507,6 +9906,7 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9507
9906
  this.pollAborts.set(accountId, controller);
9508
9907
  this.seenMessageIds.set(accountId, /* @__PURE__ */ new Set());
9509
9908
  this.consecutiveFailures.set(accountId, 0);
9909
+ this.cursorGenerations.set(accountId, 0);
9510
9910
  void this.runPollLoop(accountId, creds, controller.signal);
9511
9911
  }
9512
9912
  stopAccountWorker(accountId) {
@@ -9515,6 +9915,14 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9515
9915
  this.workerSignatures.delete(accountId);
9516
9916
  this.seenMessageIds.delete(accountId);
9517
9917
  this.consecutiveFailures.delete(accountId);
9918
+ this.pollCursors.delete(accountId);
9919
+ this.cursorGenerations.delete(accountId);
9920
+ for (const [batchId, batch] of this.pendingCursors) {
9921
+ if (batch.accountId === accountId) this.pendingCursors.delete(batchId);
9922
+ }
9923
+ const recoveryGate = this.cursorRecoveryGates.get(accountId);
9924
+ recoveryGate?.resolve();
9925
+ this.cursorRecoveryGates.delete(accountId);
9518
9926
  for (const key of Array.from(this.typingTickets.keys())) {
9519
9927
  if (key.startsWith(`${accountId}:`)) {
9520
9928
  this.typingTickets.delete(key);
@@ -9529,11 +9937,20 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9529
9937
  continue;
9530
9938
  }
9531
9939
  try {
9940
+ await this.waitForCursorRecovery(accountId, signal);
9941
+ if (signal.aborted) break;
9532
9942
  const { store } = getBridgeContext();
9533
9943
  const offsetKey = `weixin:${accountId}`;
9534
9944
  const rawOffset = store.getChannelOffset(offsetKey);
9535
- const cursor = rawOffset === "0" ? "" : rawOffset;
9945
+ const persistedCursor = rawOffset === "0" ? "" : rawOffset;
9946
+ const cursor = this.pollCursors.get(accountId) ?? persistedCursor;
9947
+ this.pollCursors.set(accountId, cursor);
9948
+ const cursorGeneration = this.cursorGenerations.get(accountId) ?? 0;
9536
9949
  const response = await getUpdates(creds, cursor);
9950
+ if (signal.aborted || !this._running) break;
9951
+ if ((this.cursorGenerations.get(accountId) ?? 0) !== cursorGeneration || this.cursorRecoveryGates.has(accountId)) {
9952
+ continue;
9953
+ }
9537
9954
  if (response.errcode === ERRCODE_SESSION_EXPIRED) {
9538
9955
  setPaused(accountId, "Session expired (errcode -14)");
9539
9956
  console.warn(`[weixin-adapter] Account ${accountId} session expired, pausing`);
@@ -9542,34 +9959,33 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9542
9959
  if (response.errcode && response.errcode !== 0) {
9543
9960
  throw new Error(`API error: ${response.errcode} ${response.errmsg || ""}`.trim());
9544
9961
  }
9545
- let batchId;
9546
- let batchCompleted = false;
9547
- if (response.msgs && response.msgs.length > 0 && response.get_updates_buf) {
9548
- batchId = this.nextBatchId++;
9549
- this.pendingCursors.set(batchId, {
9550
- offsetKey,
9551
- cursor: response.get_updates_buf,
9552
- remaining: 0,
9553
- sealed: false
9554
- });
9555
- for (const message of response.msgs) {
9556
- await this.processMessage(accountId, message, batchId);
9962
+ const messages = response.msgs ?? [];
9963
+ const nextCursor = response.get_updates_buf;
9964
+ if (nextCursor) {
9965
+ const batchId = this.createPendingCursorBatch(accountId, offsetKey, nextCursor);
9966
+ try {
9967
+ for (const message of messages) {
9968
+ await this.processMessage(accountId, message, batchId);
9969
+ }
9970
+ const batch = this.pendingCursors.get(batchId);
9971
+ if (batch) batch.sealed = true;
9972
+ this.pollCursors.set(accountId, nextCursor);
9973
+ this.maybeCommitPendingCursors(accountId);
9974
+ } catch (error) {
9975
+ const batch = this.pendingCursors.get(batchId);
9976
+ if (batch) {
9977
+ batch.failed = true;
9978
+ batch.sealed = true;
9979
+ this.ensureCursorRecoveryGate(accountId);
9980
+ }
9981
+ this.maybeCommitPendingCursors(accountId);
9982
+ throw error;
9557
9983
  }
9558
- batchCompleted = true;
9559
- } else if (response.msgs && response.msgs.length > 0) {
9560
- for (const message of response.msgs) {
9984
+ } else {
9985
+ for (const message of messages) {
9561
9986
  await this.processMessage(accountId, message);
9562
9987
  }
9563
9988
  }
9564
- if (batchId !== void 0 && response.get_updates_buf) {
9565
- const batch = this.pendingCursors.get(batchId);
9566
- if (batchCompleted && batch) {
9567
- batch.sealed = true;
9568
- this.maybeCommitPendingCursor(batchId);
9569
- } else if (!batchCompleted) {
9570
- this.pendingCursors.delete(batchId);
9571
- }
9572
- }
9573
9989
  this.consecutiveFailures.set(accountId, 0);
9574
9990
  } catch (err) {
9575
9991
  if (signal.aborted) break;
@@ -9587,12 +10003,20 @@ var WeixinAdapter = class extends BaseChannelAdapter {
9587
10003
  }
9588
10004
  async processMessage(accountId, message, batchId) {
9589
10005
  if (!message.from_user_id) return;
9590
- const messageKey = message.message_id || `seq_${message.seq}`;
10006
+ const fallbackSequence = message.seq ?? message.create_time ?? "unknown";
10007
+ const inboundMessageId = message.message_id || `weixin_${accountId}_${message.from_user_id}_${fallbackSequence}`;
10008
+ const messageKey = inboundMessageId;
10009
+ if (getBridgeContext().store.checkDedup(buildInboundDedupKey(this.channelType, inboundMessageId))) {
10010
+ return;
10011
+ }
9591
10012
  const seenIds = this.seenMessageIds.get(accountId);
9592
10013
  if (seenIds?.has(messageKey)) {
9593
10014
  return;
9594
10015
  }
9595
10016
  seenIds?.add(messageKey);
10017
+ if (batchId !== void 0) {
10018
+ this.pendingCursors.get(batchId)?.observedMessageIds.add(messageKey);
10019
+ }
9596
10020
  if (seenIds && seenIds.size > DEDUP_MAX2) {
9597
10021
  const overflow = Array.from(seenIds).slice(0, seenIds.size - DEDUP_MAX2);
9598
10022
  for (const staleKey of overflow) {
@@ -9656,7 +10080,7 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
9656
10080
  }
9657
10081
  const chatId = encodeWeixinChatId(accountId, message.from_user_id);
9658
10082
  const inbound = {
9659
- messageId: message.message_id || `weixin_${accountId}_${message.seq || Date.now()}`,
10083
+ messageId: inboundMessageId,
9660
10084
  address: {
9661
10085
  channelType: this.channelType,
9662
10086
  channelProvider: this.provider,
@@ -9686,7 +10110,10 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
9686
10110
  }
9687
10111
  if (batchId !== void 0) {
9688
10112
  const batch = this.pendingCursors.get(batchId);
9689
- if (batch) batch.remaining++;
10113
+ if (batch && !batch.messageIds.has(inbound.messageId)) {
10114
+ batch.messageIds.add(inbound.messageId);
10115
+ batch.remaining++;
10116
+ }
9690
10117
  }
9691
10118
  this.enqueueInboundMessage(inbound);
9692
10119
  const summary = attachments.length > 0 ? `[${attachments.length} attachment(s)] ${inbound.text.slice(0, 150)}` : missingVoiceTranscriptCount > 0 && !inbound.text ? "[voice transcription unavailable]" : failedCount > 0 && !inbound.text ? `[${failedCount} attachment(s) failed]` : inbound.text.slice(0, 200);
@@ -9748,13 +10175,103 @@ ${failureNote}` : attachments.length > 0 ? failureNote : text2;
9748
10175
  );
9749
10176
  });
9750
10177
  }
9751
- maybeCommitPendingCursor(updateId) {
10178
+ createPendingCursorBatch(accountId, offsetKey, cursor) {
10179
+ const batchId = this.nextBatchId++;
10180
+ this.pendingCursors.set(batchId, {
10181
+ accountId,
10182
+ offsetKey,
10183
+ cursor,
10184
+ remaining: 0,
10185
+ sealed: false,
10186
+ failed: false,
10187
+ messageIds: /* @__PURE__ */ new Set(),
10188
+ observedMessageIds: /* @__PURE__ */ new Set(),
10189
+ settledMessageIds: /* @__PURE__ */ new Set()
10190
+ });
10191
+ return batchId;
10192
+ }
10193
+ settlePendingCursorMessage(updateId, messageId, failed) {
9752
10194
  const batch = this.pendingCursors.get(updateId);
9753
- if (!batch || !batch.sealed || batch.remaining > 0) {
10195
+ if (!batch) return;
10196
+ const targetMessageId = messageId && batch.messageIds.has(messageId) ? messageId : Array.from(batch.messageIds).find((id) => !batch.settledMessageIds.has(id));
10197
+ if (!targetMessageId || batch.settledMessageIds.has(targetMessageId)) return;
10198
+ batch.settledMessageIds.add(targetMessageId);
10199
+ batch.remaining = Math.max(0, batch.remaining - 1);
10200
+ if (failed) {
10201
+ batch.failed = true;
10202
+ this.ensureCursorRecoveryGate(batch.accountId);
10203
+ }
10204
+ this.maybeCommitPendingCursors(batch.accountId);
10205
+ }
10206
+ maybeCommitPendingCursors(accountId) {
10207
+ const accountBatches = () => Array.from(this.pendingCursors.entries()).filter(([, batch]) => batch.accountId === accountId).sort(([left], [right]) => left - right);
10208
+ let batches = accountBatches();
10209
+ while (batches.length > 0) {
10210
+ const [batchId, batch] = batches[0];
10211
+ if (!batch.sealed || batch.remaining > 0 || batch.failed) break;
10212
+ try {
10213
+ getBridgeContext().store.setChannelOffset(batch.offsetKey, batch.cursor);
10214
+ } catch (error) {
10215
+ batch.failed = true;
10216
+ this.ensureCursorRecoveryGate(accountId);
10217
+ console.error(
10218
+ `[weixin-adapter] Failed to commit cursor for ${accountId}:`,
10219
+ error instanceof Error ? error.message : error
10220
+ );
10221
+ break;
10222
+ }
10223
+ this.pendingCursors.delete(batchId);
10224
+ batches = accountBatches();
10225
+ }
10226
+ batches = accountBatches();
10227
+ if (!batches.some(([, batch]) => batch.failed)) return;
10228
+ this.ensureCursorRecoveryGate(accountId);
10229
+ if (!batches.every(([, batch]) => batch.sealed && batch.remaining === 0)) return;
10230
+ const offsetKey = batches[0][1].offsetKey;
10231
+ let persistedCursor;
10232
+ try {
10233
+ const rawOffset = getBridgeContext().store.getChannelOffset(offsetKey);
10234
+ persistedCursor = rawOffset === "0" ? "" : rawOffset;
10235
+ } catch (error) {
10236
+ console.error(
10237
+ `[weixin-adapter] Failed to reload cursor for ${accountId}:`,
10238
+ error instanceof Error ? error.message : error
10239
+ );
9754
10240
  return;
9755
10241
  }
9756
- getBridgeContext().store.setChannelOffset(batch.offsetKey, batch.cursor);
9757
- this.pendingCursors.delete(updateId);
10242
+ const seenIds = this.seenMessageIds.get(accountId);
10243
+ for (const [batchId, batch] of batches) {
10244
+ for (const messageKey of batch.observedMessageIds) seenIds?.delete(messageKey);
10245
+ this.pendingCursors.delete(batchId);
10246
+ }
10247
+ this.pollCursors.set(accountId, persistedCursor);
10248
+ this.cursorGenerations.set(accountId, (this.cursorGenerations.get(accountId) ?? 0) + 1);
10249
+ const gate = this.cursorRecoveryGates.get(accountId);
10250
+ this.cursorRecoveryGates.delete(accountId);
10251
+ gate?.resolve();
10252
+ }
10253
+ ensureCursorRecoveryGate(accountId) {
10254
+ const existing = this.cursorRecoveryGates.get(accountId);
10255
+ if (existing) return existing;
10256
+ let resolve2;
10257
+ const promise = new Promise((done) => {
10258
+ resolve2 = done;
10259
+ });
10260
+ const gate = { promise, resolve: resolve2 };
10261
+ this.cursorRecoveryGates.set(accountId, gate);
10262
+ return gate;
10263
+ }
10264
+ waitForCursorRecovery(accountId, signal) {
10265
+ const gate = this.cursorRecoveryGates.get(accountId);
10266
+ if (!gate || signal.aborted) return Promise.resolve();
10267
+ return new Promise((resolve2) => {
10268
+ const finish = () => {
10269
+ signal.removeEventListener("abort", finish);
10270
+ resolve2();
10271
+ };
10272
+ signal.addEventListener("abort", finish, { once: true });
10273
+ gate.promise.then(finish, finish);
10274
+ });
9758
10275
  }
9759
10276
  filterConfiguredAccounts(accounts) {
9760
10277
  if (!this.configuredAccountId) return accounts;
@@ -9810,10 +10327,10 @@ function recordBindingChange(store, input) {
9810
10327
  }
9811
10328
 
9812
10329
  // src/desktop-sessions.ts
9813
- import fs4 from "node:fs";
10330
+ import fs5 from "node:fs";
9814
10331
  import os2 from "node:os";
9815
10332
  import path4 from "node:path";
9816
- import crypto4 from "node:crypto";
10333
+ import crypto7 from "node:crypto";
9817
10334
  import { DatabaseSync } from "node:sqlite";
9818
10335
  var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
9819
10336
  var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
@@ -9838,13 +10355,13 @@ function getDesktopStateDbPath() {
9838
10355
  const codexHome = getCodexHome();
9839
10356
  let entries;
9840
10357
  try {
9841
- entries = fs4.readdirSync(codexHome, { withFileTypes: true });
10358
+ entries = fs5.readdirSync(codexHome, { withFileTypes: true });
9842
10359
  } catch {
9843
10360
  return null;
9844
10361
  }
9845
10362
  const candidates = entries.filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/i.test(entry.name)).map((entry) => path4.join(codexHome, entry.name)).sort((left, right) => {
9846
10363
  try {
9847
- return fs4.statSync(right).mtimeMs - fs4.statSync(left).mtimeMs;
10364
+ return fs5.statSync(right).mtimeMs - fs5.statSync(left).mtimeMs;
9848
10365
  } catch {
9849
10366
  return 0;
9850
10367
  }
@@ -9869,10 +10386,10 @@ function isInternalSkillWorkspace(cwd) {
9869
10386
  }
9870
10387
  function loadSavedWorkspaceRoots() {
9871
10388
  const statePath = getCodexGlobalStatePath();
9872
- if (!fs4.existsSync(statePath)) return null;
10389
+ if (!fs5.existsSync(statePath)) return null;
9873
10390
  let parsed;
9874
10391
  try {
9875
- parsed = JSON.parse(fs4.readFileSync(statePath, "utf-8"));
10392
+ parsed = JSON.parse(fs5.readFileSync(statePath, "utf-8"));
9876
10393
  } catch {
9877
10394
  return null;
9878
10395
  }
@@ -9887,10 +10404,10 @@ function isWithinSavedWorkspaceRoots(cwd, roots) {
9887
10404
  }
9888
10405
  function loadArchivedThreadIds() {
9889
10406
  const archivedRoot = getArchivedSessionsRoot();
9890
- if (!fs4.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
10407
+ if (!fs5.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
9891
10408
  let entries;
9892
10409
  try {
9893
- entries = fs4.readdirSync(archivedRoot, { withFileTypes: true });
10410
+ entries = fs5.readdirSync(archivedRoot, { withFileTypes: true });
9894
10411
  } catch {
9895
10412
  return /* @__PURE__ */ new Set();
9896
10413
  }
@@ -9903,13 +10420,13 @@ function loadArchivedThreadIds() {
9903
10420
  return ids;
9904
10421
  }
9905
10422
  function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
9906
- const fd = fs4.openSync(filePath, "r");
10423
+ const fd = fs5.openSync(filePath, "r");
9907
10424
  try {
9908
10425
  const chunks = [];
9909
10426
  let bytesReadTotal = 0;
9910
10427
  const buffer = Buffer.alloc(4096);
9911
10428
  while (bytesReadTotal < maxBytes) {
9912
- const bytesRead = fs4.readSync(fd, buffer, 0, buffer.length, bytesReadTotal);
10429
+ const bytesRead = fs5.readSync(fd, buffer, 0, buffer.length, bytesReadTotal);
9913
10430
  if (bytesRead <= 0) break;
9914
10431
  const slice = Buffer.from(buffer.subarray(0, bytesRead));
9915
10432
  chunks.push(slice);
@@ -9922,31 +10439,31 @@ function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
9922
10439
  }
9923
10440
  return Buffer.concat(chunks).toString("utf-8").split(/\r?\n/, 1)[0] || "";
9924
10441
  } finally {
9925
- fs4.closeSync(fd);
10442
+ fs5.closeSync(fd);
9926
10443
  }
9927
10444
  }
9928
10445
  function readFilePrefix(filePath, maxBytes = MAX_SESSION_TITLE_SCAN_BYTES) {
9929
- const fd = fs4.openSync(filePath, "r");
10446
+ const fd = fs5.openSync(filePath, "r");
9930
10447
  try {
9931
10448
  const buffer = Buffer.alloc(Math.min(maxBytes, 64 * 1024));
9932
10449
  const chunks = [];
9933
10450
  let offset = 0;
9934
10451
  while (offset < maxBytes) {
9935
10452
  const bytesToRead = Math.min(buffer.length, maxBytes - offset);
9936
- const bytesRead = fs4.readSync(fd, buffer, 0, bytesToRead, offset);
10453
+ const bytesRead = fs5.readSync(fd, buffer, 0, bytesToRead, offset);
9937
10454
  if (bytesRead <= 0) break;
9938
10455
  chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
9939
10456
  offset += bytesRead;
9940
10457
  }
9941
10458
  return Buffer.concat(chunks).toString("utf-8");
9942
10459
  } finally {
9943
- fs4.closeSync(fd);
10460
+ fs5.closeSync(fd);
9944
10461
  }
9945
10462
  }
9946
10463
  function walkSessionFiles(dirPath, target) {
9947
10464
  let entries;
9948
10465
  try {
9949
- entries = fs4.readdirSync(dirPath, { withFileTypes: true });
10466
+ entries = fs5.readdirSync(dirPath, { withFileTypes: true });
9950
10467
  } catch {
9951
10468
  return;
9952
10469
  }
@@ -9969,10 +10486,10 @@ function isDesktopLike(meta) {
9969
10486
  }
9970
10487
  function loadThreadIndexEntries(archivedThreadIds) {
9971
10488
  const indexPath = getSessionIndexPath();
9972
- if (!fs4.existsSync(indexPath)) return /* @__PURE__ */ new Map();
10489
+ if (!fs5.existsSync(indexPath)) return /* @__PURE__ */ new Map();
9973
10490
  let content = "";
9974
10491
  try {
9975
- content = fs4.readFileSync(indexPath, "utf-8");
10492
+ content = fs5.readFileSync(indexPath, "utf-8");
9976
10493
  } catch {
9977
10494
  return /* @__PURE__ */ new Map();
9978
10495
  }
@@ -10012,7 +10529,7 @@ function parseUpdatedAtValue(value) {
10012
10529
  }
10013
10530
  function loadVisibleDesktopThreads(limit) {
10014
10531
  const dbPath = getDesktopStateDbPath();
10015
- if (!dbPath || !fs4.existsSync(dbPath)) return null;
10532
+ if (!dbPath || !fs5.existsSync(dbPath)) return null;
10016
10533
  let db = null;
10017
10534
  try {
10018
10535
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -10079,7 +10596,7 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
10079
10596
  }
10080
10597
  let stat;
10081
10598
  try {
10082
- stat = fs4.statSync(filePath);
10599
+ stat = fs5.statSync(filePath);
10083
10600
  } catch {
10084
10601
  return null;
10085
10602
  }
@@ -10268,25 +10785,34 @@ function formatDesktopToolName(namespaceValue, nameValue) {
10268
10785
  return namespace.endsWith("__") || namespace.endsWith("/") || namespace.endsWith(".") ? `${namespace}${name}` : `${namespace}__${name}`;
10269
10786
  }
10270
10787
  function createDesktopEventSignature(rawLine) {
10271
- return crypto4.createHash("sha1").update(rawLine).digest("hex");
10788
+ return crypto7.createHash("sha1").update(rawLine).digest("hex");
10272
10789
  }
10273
- function readFileUtf8Range(filePath, startOffset, endOffset) {
10790
+ function readCompleteUtf8LineRange(filePath, startOffset, endOffset) {
10274
10791
  const safeStart = Math.max(0, startOffset);
10275
10792
  const safeEnd = Math.max(safeStart, endOffset);
10276
10793
  const bytesToRead = safeEnd - safeStart;
10277
- if (bytesToRead <= 0) return "";
10278
- const fd = fs4.openSync(filePath, "r");
10794
+ if (bytesToRead <= 0) return { content: "", nextOffset: safeStart };
10795
+ const fd = fs5.openSync(filePath, "r");
10279
10796
  try {
10280
10797
  const buffer = Buffer.alloc(bytesToRead);
10281
10798
  let totalRead = 0;
10282
10799
  while (totalRead < bytesToRead) {
10283
- const bytesRead = fs4.readSync(fd, buffer, totalRead, bytesToRead - totalRead, safeStart + totalRead);
10800
+ const bytesRead = fs5.readSync(fd, buffer, totalRead, bytesToRead - totalRead, safeStart + totalRead);
10284
10801
  if (bytesRead <= 0) break;
10285
10802
  totalRead += bytesRead;
10286
10803
  }
10287
- return buffer.subarray(0, totalRead).toString("utf-8");
10804
+ const readBuffer = buffer.subarray(0, totalRead);
10805
+ const lastNewlineIndex = readBuffer.lastIndexOf(10);
10806
+ if (lastNewlineIndex < 0) {
10807
+ return { content: "", nextOffset: safeStart };
10808
+ }
10809
+ const consumedBytes = lastNewlineIndex + 1;
10810
+ return {
10811
+ content: readBuffer.subarray(0, consumedBytes).toString("utf-8"),
10812
+ nextOffset: safeStart + consumedBytes
10813
+ };
10288
10814
  } finally {
10289
- fs4.closeSync(fd);
10815
+ fs5.closeSync(fd);
10290
10816
  }
10291
10817
  }
10292
10818
  function isSessionEventLine(line) {
@@ -10301,6 +10827,7 @@ function isTurnContextLine(line) {
10301
10827
  var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
10302
10828
  "context_compacted",
10303
10829
  "thread_settings_applied",
10830
+ "thread_goal_updated",
10304
10831
  "thread_name_updated",
10305
10832
  "thread_rolled_back",
10306
10833
  "token_count"
@@ -10309,6 +10836,10 @@ var IGNORED_RESPONSE_ITEM_TYPES = /* @__PURE__ */ new Set([
10309
10836
  "image_generation_call",
10310
10837
  "web_search_call"
10311
10838
  ]);
10839
+ var IGNORED_TOP_LEVEL_TYPES = /* @__PURE__ */ new Set([
10840
+ "session_meta",
10841
+ "turn_context"
10842
+ ]);
10312
10843
  var TERMINAL_COMPLETION_EVENT_TYPES = /* @__PURE__ */ new Set([
10313
10844
  "task_complete",
10314
10845
  "turn.completed",
@@ -10343,7 +10874,8 @@ function isIgnoredMirrorLineKind(line) {
10343
10874
  const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
10344
10875
  return IGNORED_RESPONSE_ITEM_TYPES.has(payloadType);
10345
10876
  }
10346
- return false;
10877
+ const topLevelType = typeof line.type === "string" ? line.type.trim() : "";
10878
+ return IGNORED_TOP_LEVEL_TYPES.has(topLevelType);
10347
10879
  }
10348
10880
  function describeUnhandledMirrorLineKind(line) {
10349
10881
  if (isIgnoredMirrorLineKind(line)) return null;
@@ -10355,11 +10887,12 @@ function describeUnhandledMirrorLineKind(line) {
10355
10887
  const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
10356
10888
  return `response_item:${payloadType || "<unknown>"}`;
10357
10889
  }
10358
- return null;
10890
+ const topLevelType = typeof line.type === "string" ? line.type.trim() : "";
10891
+ return `top_level:${topLevelType || "<unknown>"}`;
10359
10892
  }
10360
10893
  function listDesktopSessions(limit) {
10361
10894
  const root = getCodexSessionsRoot();
10362
- if (!fs4.existsSync(root)) return [];
10895
+ if (!fs5.existsSync(root)) return [];
10363
10896
  const archivedThreadIds = loadArchivedThreadIds();
10364
10897
  const threadIndexEntries = loadThreadIndexEntries(archivedThreadIds);
10365
10898
  const savedWorkspaceRoots = loadSavedWorkspaceRoots();
@@ -10397,9 +10930,17 @@ function listDesktopSessions(limit) {
10397
10930
  }
10398
10931
  return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.max(1, Math.floor(limit)) : void 0);
10399
10932
  }
10933
+ var DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS = 1e3;
10934
+ var desktopSessionLookupCache = null;
10400
10935
  function getDesktopSessionByThreadId(threadId) {
10401
- const sessions = listDesktopSessions(200);
10402
- return sessions.find((session) => session.threadId === threadId) || null;
10936
+ const timestamp = Date.now();
10937
+ if (!desktopSessionLookupCache || desktopSessionLookupCache.expiresAt <= timestamp) {
10938
+ desktopSessionLookupCache = {
10939
+ expiresAt: timestamp + DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS,
10940
+ sessions: new Map(listDesktopSessions().map((session) => [session.threadId, session]))
10941
+ };
10942
+ }
10943
+ return desktopSessionLookupCache.sessions.get(threadId) || null;
10403
10944
  }
10404
10945
  function extractDesktopMessageText(line) {
10405
10946
  const parts = line.payload?.content?.map((item) => item && typeof item.text === "string" ? item.text : "").filter(Boolean) || [];
@@ -10691,6 +11232,23 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
10691
11232
  });
10692
11233
  return true;
10693
11234
  }
11235
+ if (parsed.payload?.type === "message" && parsed.payload.role === "user") {
11236
+ const text2 = extractDesktopMessageText(parsed);
11237
+ if (!text2) return true;
11238
+ const previous = records.at(-1);
11239
+ if (previous?.type === "message" && previous.role === "user" && previous.content === text2 && previous.turnId === (activeTurnId || void 0)) {
11240
+ return true;
11241
+ }
11242
+ records.push({
11243
+ signature,
11244
+ type: "message",
11245
+ role: "user",
11246
+ content: text2,
11247
+ timestamp,
11248
+ ...activeTurnId ? { turnId: activeTurnId } : {}
11249
+ });
11250
+ return true;
11251
+ }
10694
11252
  if (parsed.payload?.type === "tool_search_call") {
10695
11253
  const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10696
11254
  records.push({
@@ -10920,16 +11478,16 @@ ${event.content}` : event.content
10920
11478
  function readDesktopSessionEventStreamByFilePath(filePath) {
10921
11479
  let content = "";
10922
11480
  try {
10923
- content = fs4.readFileSync(filePath, "utf-8");
11481
+ content = fs5.readFileSync(filePath, "utf-8");
10924
11482
  } catch {
10925
11483
  return [];
10926
11484
  }
10927
11485
  return parseDesktopSessionEventText(content, "", true).events;
10928
11486
  }
10929
11487
  function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
10930
- let content = "";
11488
+ let range;
10931
11489
  try {
10932
- content = readFileUtf8Range(filePath, startOffset, endOffset);
11490
+ range = readCompleteUtf8LineRange(filePath, startOffset, endOffset);
10933
11491
  } catch {
10934
11492
  return {
10935
11493
  records: [],
@@ -10940,10 +11498,16 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
10940
11498
  unknownKinds: []
10941
11499
  };
10942
11500
  }
10943
- const parsed = parseDesktopMirrorRecordText(content, trailingText, false, currentTurnId, currentSpecialCallIds);
11501
+ const parsed = parseDesktopMirrorRecordText(
11502
+ range.content,
11503
+ trailingText,
11504
+ false,
11505
+ currentTurnId,
11506
+ currentSpecialCallIds
11507
+ );
10944
11508
  return {
10945
11509
  records: parsed.records,
10946
- nextOffset: Math.max(startOffset, endOffset),
11510
+ nextOffset: range.nextOffset,
10947
11511
  trailingText: parsed.trailingText,
10948
11512
  nextTurnId: parsed.nextTurnId,
10949
11513
  nextSpecialCallIds: parsed.nextSpecialCallIds,
@@ -11103,14 +11667,14 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
11103
11667
  }
11104
11668
 
11105
11669
  // src/internal-sessions.ts
11106
- import fs5 from "node:fs";
11670
+ import fs6 from "node:fs";
11107
11671
  import path6 from "node:path";
11108
11672
  var DRAFT_TTL_MS = 24 * 60 * 60 * 1e3;
11109
11673
  var MAX_HIDDEN_DRAFT_SESSIONS = 64;
11110
11674
  var INTERNAL_SESSION_ROOT = path6.join(CTI_HOME, "runtime", "internal-sessions");
11111
11675
  var DRAFT_SESSION_PREFIX = "Draft";
11112
11676
  function ensureDirectory(dirPath) {
11113
- fs5.mkdirSync(dirPath, { recursive: true });
11677
+ fs6.mkdirSync(dirPath, { recursive: true });
11114
11678
  }
11115
11679
  function sanitizePathSlug(raw) {
11116
11680
  return raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "scratch";
@@ -12874,15 +13438,32 @@ function createAdapterRuntime(getState2, deps) {
12874
13438
  const msg = await adapter.consumeOne();
12875
13439
  if (!msg) continue;
12876
13440
  if (msg.callbackData || msg.text.trim().startsWith("/") || deps.isNumericPermissionShortcut(adapter.provider, msg.text.trim(), msg.address.chatId)) {
12877
- await deps.handleMessage(adapter, msg);
13441
+ try {
13442
+ await deps.handleMessage(adapter, msg);
13443
+ } catch (error) {
13444
+ if (msg.updateId != null) {
13445
+ adapter.rejectUpdate?.(msg.updateId, msg.messageId);
13446
+ }
13447
+ throw error;
13448
+ }
12878
13449
  } else {
12879
- const sessionId = deps.resolveSessionIdForMessage(msg);
12880
- deps.processWithSessionLock(
12881
- sessionId,
12882
- () => deps.handleMessage(adapter, msg)
12883
- ).catch((err) => {
12884
- console.error(`[bridge-manager] Session ${sessionId.slice(0, 8)} error:`, err);
12885
- });
13450
+ try {
13451
+ const sessionId = deps.resolveSessionIdForMessage(msg);
13452
+ deps.processWithSessionLock(
13453
+ sessionId,
13454
+ () => deps.handleMessage(adapter, msg)
13455
+ ).catch((err) => {
13456
+ if (msg.updateId != null) {
13457
+ adapter.rejectUpdate?.(msg.updateId, msg.messageId);
13458
+ }
13459
+ console.error(`[bridge-manager] Session ${sessionId.slice(0, 8)} error:`, err);
13460
+ });
13461
+ } catch (error) {
13462
+ if (msg.updateId != null) {
13463
+ adapter.rejectUpdate?.(msg.updateId, msg.messageId);
13464
+ }
13465
+ throw error;
13466
+ }
12886
13467
  }
12887
13468
  } catch (err) {
12888
13469
  if (abort.signal.aborted) break;
@@ -12925,18 +13506,64 @@ function createAdapterRuntime(getState2, deps) {
12925
13506
  }
12926
13507
 
12927
13508
  // src/lib/bridge/bridge-session-support.ts
12928
- import fs7 from "node:fs";
13509
+ import fs8 from "node:fs";
12929
13510
  import path10 from "node:path";
12930
13511
 
12931
13512
  // src/codex-models.ts
12932
- import fs6 from "node:fs";
13513
+ import fs7 from "node:fs";
12933
13514
  import os3 from "node:os";
12934
13515
  import path9 from "node:path";
12935
13516
  var DEFAULT_CODEX_CONFIG_PATH = path9.join(os3.homedir(), ".codex", "config.toml");
12936
13517
  var DEFAULT_CODEX_MODELS_CACHE_PATH = path9.join(os3.homedir(), ".codex", "models_cache.json");
13518
+ var REASONING_LEVEL_DESCRIPTIONS = {
13519
+ low: "Fast responses with lighter reasoning",
13520
+ medium: "Balances speed and reasoning depth for everyday tasks",
13521
+ high: "Greater reasoning depth for complex problems",
13522
+ xhigh: "Extra high reasoning depth for complex problems",
13523
+ max: "Maximum reasoning depth for very hard problems",
13524
+ ultra: "Ultra reasoning depth for the hardest problems"
13525
+ };
13526
+ function buildReasoningLevels(efforts) {
13527
+ return efforts.map((effort) => ({
13528
+ effort,
13529
+ description: REASONING_LEVEL_DESCRIPTIONS[effort] || ""
13530
+ }));
13531
+ }
13532
+ function cloneCodexModel(model) {
13533
+ return {
13534
+ ...model,
13535
+ supportedReasoningLevels: model.supportedReasoningLevels.map((level) => ({ ...level }))
13536
+ };
13537
+ }
13538
+ var KNOWN_CODEX_MODEL_FALLBACKS = [
13539
+ {
13540
+ slug: "gpt-5.6-sol",
13541
+ displayName: "GPT-5.6-Sol",
13542
+ visibility: "list",
13543
+ supportedInApi: true,
13544
+ defaultReasoningLevel: "low",
13545
+ supportedReasoningLevels: buildReasoningLevels(["low", "medium", "high", "xhigh", "max", "ultra"])
13546
+ },
13547
+ {
13548
+ slug: "gpt-5.6-terra",
13549
+ displayName: "GPT-5.6-Terra",
13550
+ visibility: "list",
13551
+ supportedInApi: true,
13552
+ defaultReasoningLevel: "medium",
13553
+ supportedReasoningLevels: buildReasoningLevels(["low", "medium", "high", "xhigh", "max", "ultra"])
13554
+ },
13555
+ {
13556
+ slug: "gpt-5.6-luna",
13557
+ displayName: "GPT-5.6-Luna",
13558
+ visibility: "list",
13559
+ supportedInApi: true,
13560
+ defaultReasoningLevel: "medium",
13561
+ supportedReasoningLevels: buildReasoningLevels(["low", "medium", "high", "xhigh", "max"])
13562
+ }
13563
+ ];
12937
13564
  function readConfiguredCodexModel(configPath = DEFAULT_CODEX_CONFIG_PATH) {
12938
13565
  try {
12939
- const raw = fs6.readFileSync(configPath, "utf-8");
13566
+ const raw = fs7.readFileSync(configPath, "utf-8");
12940
13567
  let inSection = false;
12941
13568
  for (const line of raw.split(/\r?\n/)) {
12942
13569
  const trimmed = line.trim();
@@ -12980,7 +13607,7 @@ function parseSupportedReasoningLevels(value) {
12980
13607
  }
12981
13608
  function listCachedCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
12982
13609
  try {
12983
- const raw = fs6.readFileSync(cachePath, "utf-8");
13610
+ const raw = fs7.readFileSync(cachePath, "utf-8");
12984
13611
  const parsed = JSON.parse(raw);
12985
13612
  if (!Array.isArray(parsed.models)) return [];
12986
13613
  const seen = /* @__PURE__ */ new Set();
@@ -13005,13 +13632,50 @@ function listCachedCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
13005
13632
  return [];
13006
13633
  }
13007
13634
  }
13008
- function listSelectableCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
13009
- return listCachedCodexModels(cachePath).filter((model) => model.visibility !== "hide");
13635
+ function buildSyntheticCodexModel(slug) {
13636
+ const normalized = slug.trim();
13637
+ if (!normalized) return null;
13638
+ return {
13639
+ slug: normalized,
13640
+ displayName: normalized,
13641
+ visibility: "list",
13642
+ supportedInApi: true,
13643
+ supportedReasoningLevels: []
13644
+ };
13010
13645
  }
13011
- function findSelectableCodexModel(slug, cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
13646
+ function listAvailableCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH, additionalModelSlugs = []) {
13647
+ const cachedModels = listCachedCodexModels(cachePath);
13648
+ const hiddenCachedSlugs = new Set(
13649
+ cachedModels.filter((model) => model.visibility === "hide").map((model) => model.slug)
13650
+ );
13651
+ const merged = [];
13652
+ const seen = /* @__PURE__ */ new Set();
13653
+ const addModel = (model, force = false) => {
13654
+ if (!force && hiddenCachedSlugs.has(model.slug)) return;
13655
+ if (seen.has(model.slug)) return;
13656
+ seen.add(model.slug);
13657
+ merged.push(cloneCodexModel(model));
13658
+ };
13659
+ for (const model of cachedModels) {
13660
+ if (model.visibility !== "hide") {
13661
+ addModel(model, true);
13662
+ }
13663
+ }
13664
+ for (const model of KNOWN_CODEX_MODEL_FALLBACKS) {
13665
+ addModel(model);
13666
+ }
13667
+ for (const slug of additionalModelSlugs) {
13668
+ const syntheticModel = buildSyntheticCodexModel(slug);
13669
+ if (syntheticModel) {
13670
+ addModel(syntheticModel, true);
13671
+ }
13672
+ }
13673
+ return merged;
13674
+ }
13675
+ function findAvailableCodexModel(slug, cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH, additionalModelSlugs = []) {
13012
13676
  const normalized = slug.trim();
13013
13677
  if (!normalized) return null;
13014
- return listSelectableCodexModels(cachePath).find((model) => model.slug === normalized) || null;
13678
+ return listAvailableCodexModels(cachePath, additionalModelSlugs).find((model) => model.slug === normalized) || null;
13015
13679
  }
13016
13680
  function isCliOnlyCodexModel(model) {
13017
13681
  return !!model && model.supportedInApi === false;
@@ -13019,7 +13683,7 @@ function isCliOnlyCodexModel(model) {
13019
13683
 
13020
13684
  // src/lib/bridge/bridge-session-support.ts
13021
13685
  init_runtime_options();
13022
- var AVAILABLE_CODEX_MODELS = listSelectableCodexModels();
13686
+ var AVAILABLE_CODEX_MODELS = listAvailableCodexModels();
13023
13687
  var AVAILABLE_CODEX_MODEL_MAP = new Map(AVAILABLE_CODEX_MODELS.map((model) => [model.slug, model]));
13024
13688
  function getDisplayedDesktopThreads(limit) {
13025
13689
  try {
@@ -13068,7 +13732,7 @@ function getAvailableModelChoicesText() {
13068
13732
  return `\u53EF\u9009\u6A21\u578B\uFF1A${AVAILABLE_CODEX_MODELS.map((model) => formatDisplayedModel(model.slug)).join("\u3001")}`;
13069
13733
  }
13070
13734
  function getSelectableCodexModel(slug) {
13071
- return AVAILABLE_CODEX_MODEL_MAP.get(slug) || findSelectableCodexModel(slug);
13735
+ return AVAILABLE_CODEX_MODEL_MAP.get(slug) || findAvailableCodexModel(slug);
13072
13736
  }
13073
13737
  function resolveNewWorkingDirectory(rawArgs) {
13074
13738
  const trimmed = rawArgs.trim();
@@ -13129,7 +13793,7 @@ function resolveNewSessionWorkingDirectory(rawArgs, binding, session) {
13129
13793
  return { ok: true, workDir: validated };
13130
13794
  }
13131
13795
  function ensureWorkingDirectoryExists(workDir) {
13132
- fs7.mkdirSync(workDir, { recursive: true });
13796
+ fs8.mkdirSync(workDir, { recursive: true });
13133
13797
  }
13134
13798
  function resetDraftSession2(address) {
13135
13799
  const { store } = getBridgeContext();
@@ -13299,14 +13963,20 @@ async function deliverResponse(adapter, address, responseText, sessionId, replyT
13299
13963
  replyToMessageId
13300
13964
  }, { sessionId });
13301
13965
  if (!attachmentResult.ok) {
13302
- return deliverTextResponse(
13966
+ const attachmentError = attachmentResult.error || "\u672A\u77E5\u9519\u8BEF";
13967
+ const noticeResult = await deliverTextResponse(
13303
13968
  adapter,
13304
13969
  address,
13305
13970
  `\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}
13306
- ${attachmentResult.error || "\u672A\u77E5\u9519\u8BEF"}`,
13971
+ ${attachmentError}`,
13307
13972
  sessionId,
13308
13973
  replyToMessageId
13309
13974
  );
13975
+ return {
13976
+ ok: false,
13977
+ messageId: noticeResult.messageId,
13978
+ error: noticeResult.ok ? `\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}: ${attachmentError}` : `\u9644\u4EF6\u53D1\u9001\u5931\u8D25\uFF1A${attachment.path}: ${attachmentError}; \u9519\u8BEF\u63D0\u793A\u4E5F\u53D1\u9001\u5931\u8D25\uFF1A${noticeResult.error || "\u672A\u77E5\u9519\u8BEF"}`
13979
+ };
13310
13980
  }
13311
13981
  lastResult = attachmentResult;
13312
13982
  }
@@ -14032,9 +14702,9 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
14032
14702
  import path13 from "node:path";
14033
14703
 
14034
14704
  // src/lib/bridge/conversation-engine.ts
14035
- import fs8 from "fs";
14705
+ import fs9 from "fs";
14036
14706
  import path12 from "path";
14037
- import crypto5 from "crypto";
14707
+ import crypto8 from "crypto";
14038
14708
 
14039
14709
  // src/lib/bridge/turns/final-response-artifacts.ts
14040
14710
  function attachmentKey(attachment) {
@@ -14112,6 +14782,74 @@ async function consumeSseEvents(stream, onEvent) {
14112
14782
  }
14113
14783
 
14114
14784
  // src/lib/bridge/conversation-engine.ts
14785
+ var MAX_INBOUND_ATTACHMENT_BYTES = 100 * 1024 * 1024;
14786
+ var MAX_INBOUND_ATTACHMENTS_TOTAL_BYTES = 120 * 1024 * 1024;
14787
+ function validateInboundAttachmentSizes(files, maxFileBytes = MAX_INBOUND_ATTACHMENT_BYTES, maxTotalBytes = MAX_INBOUND_ATTACHMENTS_TOTAL_BYTES) {
14788
+ if (!files || files.length === 0) return null;
14789
+ let totalBytes = 0;
14790
+ for (const file of files) {
14791
+ const encodedSize = typeof file.data === "string" ? Buffer.byteLength(file.data, "base64") : 0;
14792
+ const fileSize = Math.max(0, Number(file.size) || 0, encodedSize);
14793
+ if (fileSize > maxFileBytes) {
14794
+ return `Attachment "${file.name}" is too large (${fileSize} bytes; max ${maxFileBytes} bytes).`;
14795
+ }
14796
+ totalBytes += fileSize;
14797
+ if (totalBytes > maxTotalBytes) {
14798
+ return `Attachments are too large in total (${totalBytes} bytes; max ${maxTotalBytes} bytes).`;
14799
+ }
14800
+ }
14801
+ return null;
14802
+ }
14803
+ var DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS = [
14804
+ 5e3,
14805
+ 1e4,
14806
+ 15e3,
14807
+ 3e4,
14808
+ 3e4
14809
+ ];
14810
+ function isSessionBusyErrorMessage(message) {
14811
+ return (message || "").toLowerCase().includes("session is busy processing another request");
14812
+ }
14813
+ function isDesktopBackedSessionForBusyRetry(session) {
14814
+ if (session?.thread_origin !== "desktop") return false;
14815
+ return Boolean(session.desktop_thread_id || session.sdk_session_id || session.codex_thread_id);
14816
+ }
14817
+ function getDesktopBusyRetryDelaysMs() {
14818
+ const configured = process.env.CTI_DESKTOP_BUSY_RETRY_DELAYS_MS;
14819
+ if (!configured?.trim()) return DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS;
14820
+ const delays = configured.split(",").map((part) => parseInt(part.trim(), 10)).filter((value) => Number.isFinite(value) && value >= 0);
14821
+ return delays.length > 0 ? delays : DEFAULT_DESKTOP_BUSY_RETRY_DELAYS_MS;
14822
+ }
14823
+ function isRetryableDesktopBusyResult(result) {
14824
+ return result.hasError && (result.errorCode === "session_busy" || isSessionBusyErrorMessage(result.errorMessage)) && !result.responseText.trim() && result.outboundAttachments.length === 0 && result.permissionRequests.length === 0;
14825
+ }
14826
+ function formatDesktopBusyRetryNote(attempt, maxAttempts, delayMs) {
14827
+ const delaySeconds = Math.ceil(delayMs / 1e3);
14828
+ const delayText = delaySeconds > 0 ? `${delaySeconds} \u79D2\u540E` : "\u9A6C\u4E0A";
14829
+ return `Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C${delayText}\u91CD\u8BD5\uFF08${attempt}/${maxAttempts}\uFF09\u3002`;
14830
+ }
14831
+ function buildDesktopBusyExhaustedMessage(originalMessage) {
14832
+ const detail = originalMessage?.trim();
14833
+ return detail ? `Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002\u539F\u59CB\u9519\u8BEF\uFF1A${detail}` : "Codex Desktop thread \u4ECD\u5728\u5904\u7406\u4E0A\u4E00\u8F6E\u8BF7\u6C42\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
14834
+ }
14835
+ function waitForRetryDelay(delayMs, signal) {
14836
+ if (signal.aborted) return Promise.resolve(false);
14837
+ if (delayMs <= 0) return Promise.resolve(true);
14838
+ return new Promise((resolve2) => {
14839
+ let timer = null;
14840
+ const finish = (completed) => {
14841
+ if (timer) {
14842
+ clearTimeout(timer);
14843
+ timer = null;
14844
+ }
14845
+ signal.removeEventListener("abort", onAbort);
14846
+ resolve2(completed);
14847
+ };
14848
+ const onAbort = () => finish(false);
14849
+ timer = setTimeout(() => finish(true), delayMs);
14850
+ signal.addEventListener("abort", onAbort, { once: true });
14851
+ });
14852
+ }
14115
14853
  function resolveReasoningEffort(store, session) {
14116
14854
  return normalizeReasoningEffort(
14117
14855
  session?.reasoning_effort || store.getSetting("bridge_codex_reasoning_effort")
@@ -14154,7 +14892,19 @@ ${attachmentSupplement}` : attachmentSupplement;
14154
14892
  async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onTaskEvent, onStatusNote, onPromptPrepared) {
14155
14893
  const { store, llm } = getBridgeContext();
14156
14894
  const sessionId = binding.codepilotSessionId;
14157
- const lockId = crypto5.randomBytes(8).toString("hex");
14895
+ const attachmentValidationError = validateInboundAttachmentSizes(files);
14896
+ if (attachmentValidationError) {
14897
+ return {
14898
+ responseText: "",
14899
+ outboundAttachments: [],
14900
+ tokenUsage: null,
14901
+ hasError: true,
14902
+ errorMessage: attachmentValidationError,
14903
+ permissionRequests: [],
14904
+ sdkSessionId: null
14905
+ };
14906
+ }
14907
+ const lockId = crypto8.randomBytes(8).toString("hex");
14158
14908
  const lockAcquired = store.acquireSessionLock(sessionId, lockId, `bridge-${binding.channelType}`, 600);
14159
14909
  if (!lockAcquired) {
14160
14910
  return {
@@ -14163,6 +14913,7 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
14163
14913
  tokenUsage: null,
14164
14914
  hasError: true,
14165
14915
  errorMessage: "Session is busy processing another request",
14916
+ errorCode: "session_busy",
14166
14917
  permissionRequests: [],
14167
14918
  sdkSessionId: null
14168
14919
  };
@@ -14184,16 +14935,21 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
14184
14935
  let persistedFileMeta = [];
14185
14936
  if (files && files.length > 0) {
14186
14937
  if (workDir) {
14938
+ const createdFilePaths = [];
14187
14939
  try {
14188
14940
  const uploadDir = path12.join(workDir, ".codepilot-uploads");
14189
- if (!fs8.existsSync(uploadDir)) {
14190
- fs8.mkdirSync(uploadDir, { recursive: true });
14941
+ if (!fs9.existsSync(uploadDir)) {
14942
+ fs9.mkdirSync(uploadDir, { recursive: true });
14191
14943
  }
14192
14944
  const fileMeta = files.map((f) => {
14193
14945
  const safeName = path12.basename(f.name).replace(/[^a-zA-Z0-9._-]/g, "_");
14194
- const filePath = path12.join(uploadDir, `${Date.now()}-${safeName}`);
14946
+ const filePath = path12.join(uploadDir, `${crypto8.randomUUID()}-${safeName || "attachment.bin"}`);
14195
14947
  const buffer = Buffer.from(f.data, "base64");
14196
- fs8.writeFileSync(filePath, buffer);
14948
+ if (buffer.length > MAX_INBOUND_ATTACHMENT_BYTES) {
14949
+ throw new Error(`Attachment "${f.name}" exceeds the maximum size after decoding`);
14950
+ }
14951
+ fs9.writeFileSync(filePath, buffer);
14952
+ createdFilePaths.push(filePath);
14197
14953
  return { id: f.id, name: f.name, type: f.type, size: buffer.length, filePath };
14198
14954
  });
14199
14955
  persistedFileMeta = fileMeta;
@@ -14203,6 +14959,12 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
14203
14959
  });
14204
14960
  savedContent = `<!--files:${JSON.stringify(fileMeta)}-->${text2}`;
14205
14961
  } catch (err) {
14962
+ for (const filePath of createdFilePaths) {
14963
+ try {
14964
+ fs9.rmSync(filePath, { force: true });
14965
+ } catch {
14966
+ }
14967
+ }
14206
14968
  console.warn("[conversation-engine] Failed to persist file attachments:", err instanceof Error ? err.message : err);
14207
14969
  savedContent = `[${files.length} attachment(s) attached] ${text2}`;
14208
14970
  }
@@ -14248,37 +15010,65 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
14248
15010
  abortSignal.addEventListener("abort", () => abortController.abort(), { once: true });
14249
15011
  }
14250
15012
  }
14251
- const stream = llm.streamChat({
14252
- prompt: promptText,
14253
- sessionId,
14254
- sdkSessionId: binding.sdkSessionId || void 0,
14255
- model: effectiveModel,
14256
- forceModel: !binding.sdkSessionId && Boolean(effectiveModel),
14257
- sandboxMode,
14258
- modelReasoningEffort,
14259
- systemPrompt: session?.system_prompt || void 0,
14260
- workingDirectory: workDir || void 0,
14261
- abortController,
14262
- permissionMode,
14263
- provider: resolvedProvider,
14264
- conversationHistory: historyMsgs,
14265
- files: llmFiles,
14266
- onRuntimeStatusChange: (status) => {
14267
- try {
14268
- store.setSessionRuntimeStatus(sessionId, status);
14269
- } catch {
15013
+ const desktopBusyRetryDelaysMs = isDesktopBackedSessionForBusyRetry(session) ? getDesktopBusyRetryDelaysMs() : [];
15014
+ let desktopBusyRetryCount = 0;
15015
+ while (true) {
15016
+ const stream = llm.streamChat({
15017
+ prompt: promptText,
15018
+ sessionId,
15019
+ sdkSessionId: binding.sdkSessionId || void 0,
15020
+ model: effectiveModel,
15021
+ forceModel: !binding.sdkSessionId && Boolean(effectiveModel),
15022
+ sandboxMode,
15023
+ modelReasoningEffort,
15024
+ systemPrompt: session?.system_prompt || void 0,
15025
+ workingDirectory: workDir || void 0,
15026
+ abortController,
15027
+ permissionMode,
15028
+ provider: resolvedProvider,
15029
+ conversationHistory: historyMsgs,
15030
+ files: llmFiles,
15031
+ onRuntimeStatusChange: (status) => {
15032
+ try {
15033
+ store.setSessionRuntimeStatus(sessionId, status);
15034
+ } catch {
15035
+ }
14270
15036
  }
15037
+ });
15038
+ const result = await consumeStream(
15039
+ stream,
15040
+ sessionId,
15041
+ onPermissionRequest,
15042
+ onPartialText,
15043
+ onToolEvent,
15044
+ onTaskEvent,
15045
+ onStatusNote
15046
+ );
15047
+ if (!isRetryableDesktopBusyResult(result) || desktopBusyRetryDelaysMs.length === 0) {
15048
+ return result;
14271
15049
  }
14272
- });
14273
- return await consumeStream(
14274
- stream,
14275
- sessionId,
14276
- onPermissionRequest,
14277
- onPartialText,
14278
- onToolEvent,
14279
- onTaskEvent,
14280
- onStatusNote
14281
- );
15050
+ if (desktopBusyRetryCount >= desktopBusyRetryDelaysMs.length) {
15051
+ return {
15052
+ ...result,
15053
+ errorMessage: buildDesktopBusyExhaustedMessage(result.errorMessage)
15054
+ };
15055
+ }
15056
+ const delayMs = desktopBusyRetryDelaysMs[desktopBusyRetryCount];
15057
+ desktopBusyRetryCount += 1;
15058
+ onStatusNote?.(formatDesktopBusyRetryNote(
15059
+ desktopBusyRetryCount,
15060
+ desktopBusyRetryDelaysMs.length,
15061
+ delayMs
15062
+ ));
15063
+ const shouldRetry2 = await waitForRetryDelay(delayMs, abortController.signal);
15064
+ if (!shouldRetry2) {
15065
+ return {
15066
+ ...result,
15067
+ errorMessage: "Task stopped by user",
15068
+ errorCode: void 0
15069
+ };
15070
+ }
15071
+ }
14282
15072
  } finally {
14283
15073
  clearInterval(renewalInterval);
14284
15074
  store.releaseSessionLock(sessionId, lockId);
@@ -14293,10 +15083,36 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
14293
15083
  let tokenUsage = null;
14294
15084
  let hasError = false;
14295
15085
  let errorMessage = "";
15086
+ let errorCode;
14296
15087
  const seenToolResultIds = /* @__PURE__ */ new Set();
14297
15088
  const permissionRequests = [];
14298
15089
  let capturedSdkSessionId = null;
14299
- const outboundAttachments = [];
15090
+ const finalizeConsumedContent = () => {
15091
+ if (currentText.trim()) {
15092
+ contentBlocks.push({ type: "text", text: currentText });
15093
+ currentText = "";
15094
+ }
15095
+ const outboundAttachments = [];
15096
+ for (const block2 of contentBlocks) {
15097
+ if (block2.type !== "text") continue;
15098
+ const parsed = collectFinalResponseArtifacts(block2.text);
15099
+ block2.text = parsed.text;
15100
+ outboundAttachments.push(...parsed.attachments);
15101
+ }
15102
+ if (contentBlocks.length > 0) {
15103
+ const hasToolBlocks = contentBlocks.some(
15104
+ (block2) => block2.type === "tool_use" || block2.type === "tool_result"
15105
+ );
15106
+ const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((block2) => block2.type === "text").map((block2) => block2.text).join("\n\n").trim();
15107
+ if (content) {
15108
+ store.addMessage(sessionId, "assistant", content, tokenUsage ? JSON.stringify(tokenUsage) : null);
15109
+ }
15110
+ }
15111
+ return {
15112
+ responseText: contentBlocks.filter((block2) => block2.type === "text").map((block2) => block2.text).join("").trim(),
15113
+ outboundAttachments: dedupeOutboundAttachments(outboundAttachments)
15114
+ };
15115
+ };
14300
15116
  try {
14301
15117
  await consumeSseEvents(stream, async (event) => {
14302
15118
  switch (event.type) {
@@ -14424,6 +15240,9 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
14424
15240
  case "error":
14425
15241
  hasError = true;
14426
15242
  errorMessage = event.data || "Unknown error";
15243
+ if (isSessionBusyErrorMessage(errorMessage)) {
15244
+ errorCode = "session_busy";
15245
+ }
14427
15246
  break;
14428
15247
  case "result": {
14429
15248
  try {
@@ -14440,54 +15259,28 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
14440
15259
  }
14441
15260
  }
14442
15261
  });
14443
- if (currentText.trim()) {
14444
- contentBlocks.push({ type: "text", text: currentText });
14445
- }
14446
- if (contentBlocks.length > 0) {
14447
- for (const block2 of contentBlocks) {
14448
- if (block2.type !== "text") continue;
14449
- const parsed = collectFinalResponseArtifacts(block2.text);
14450
- block2.text = parsed.text;
14451
- outboundAttachments.push(...parsed.attachments);
14452
- }
14453
- const hasToolBlocks = contentBlocks.some(
14454
- (b) => b.type === "tool_use" || b.type === "tool_result"
14455
- );
14456
- const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("\n\n").trim();
14457
- if (content) {
14458
- store.addMessage(sessionId, "assistant", content, tokenUsage ? JSON.stringify(tokenUsage) : null);
14459
- }
14460
- }
14461
- const responseText = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
15262
+ const finalizedContent = finalizeConsumedContent();
14462
15263
  return {
14463
- responseText,
14464
- outboundAttachments: dedupeOutboundAttachments(outboundAttachments),
15264
+ responseText: finalizedContent.responseText,
15265
+ outboundAttachments: finalizedContent.outboundAttachments,
14465
15266
  tokenUsage,
14466
15267
  hasError,
14467
15268
  errorMessage,
15269
+ errorCode,
14468
15270
  permissionRequests,
14469
15271
  sdkSessionId: capturedSdkSessionId
14470
15272
  };
14471
15273
  } catch (e) {
14472
- if (currentText.trim()) {
14473
- contentBlocks.push({ type: "text", text: currentText });
14474
- }
14475
- if (contentBlocks.length > 0) {
14476
- const hasToolBlocks = contentBlocks.some(
14477
- (b) => b.type === "tool_use" || b.type === "tool_result"
14478
- );
14479
- const content = hasToolBlocks ? JSON.stringify(contentBlocks) : contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("\n\n").trim();
14480
- if (content) {
14481
- store.addMessage(sessionId, "assistant", content);
14482
- }
14483
- }
15274
+ const finalizedContent = finalizeConsumedContent();
14484
15275
  const isAbort = e instanceof DOMException && e.name === "AbortError" || e instanceof Error && e.name === "AbortError";
15276
+ const fallbackErrorMessage = isAbort ? "Task stopped by user" : e instanceof Error ? e.message : "Stream consumption error";
14485
15277
  return {
14486
- responseText: "",
14487
- outboundAttachments: [],
15278
+ responseText: finalizedContent.responseText,
15279
+ outboundAttachments: finalizedContent.outboundAttachments,
14488
15280
  tokenUsage,
14489
15281
  hasError: true,
14490
- errorMessage: isAbort ? "Task stopped by user" : e instanceof Error ? e.message : "Stream consumption error",
15282
+ errorMessage: fallbackErrorMessage,
15283
+ errorCode: isSessionBusyErrorMessage(fallbackErrorMessage) ? "session_busy" : void 0,
14491
15284
  permissionRequests,
14492
15285
  sdkSessionId: capturedSdkSessionId
14493
15286
  };
@@ -15123,8 +15916,28 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
15123
15916
  }
15124
15917
  let finalOutcome = "failed";
15125
15918
  let finalOutcomeDetail;
15919
+ let finalDeliveryError;
15126
15920
  let shouldRecordHealthEnd = true;
15127
15921
  let forceStopStarted = false;
15922
+ const deliverFinalPayload = async (response, options) => {
15923
+ try {
15924
+ const result = await deliverFinalResponse({
15925
+ adapter,
15926
+ address: msg.address,
15927
+ sessionId: binding.codepilotSessionId,
15928
+ replyToMessageId: msg.messageId,
15929
+ deliverResponse: deps.deliverResponse
15930
+ }, response, options);
15931
+ if (result.ok) return true;
15932
+ finalDeliveryError = `\u6700\u7EC8\u56DE\u590D\u6295\u9012\u5931\u8D25\uFF1A${result.error || "\u672A\u77E5\u9519\u8BEF"}`;
15933
+ } catch (error) {
15934
+ finalDeliveryError = `\u6700\u7EC8\u56DE\u590D\u6295\u9012\u5F02\u5E38\uFF1A${error instanceof Error ? error.message : String(error)}`;
15935
+ }
15936
+ console.error(`[interactive-message-runner] ${finalDeliveryError}`);
15937
+ finalOutcome = "failed";
15938
+ finalOutcomeDetail = finalDeliveryError;
15939
+ return false;
15940
+ };
15128
15941
  taskState.forceStop = async (detail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002") => {
15129
15942
  if (forceStopStarted) return true;
15130
15943
  forceStopStarted = true;
@@ -15212,13 +16025,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
15212
16025
  });
15213
16026
  const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalResponse2.text);
15214
16027
  if (hasFinalResponsePayload(terminalResponse2)) {
15215
- await deliverFinalResponse({
15216
- adapter,
15217
- address: msg.address,
15218
- sessionId: binding.codepilotSessionId,
15219
- replyToMessageId: msg.messageId,
15220
- deliverResponse: deps.deliverResponse
15221
- }, terminalResponse2, { skipText: cardFinalized2 });
16028
+ await deliverFinalPayload(terminalResponse2, { skipText: cardFinalized2 });
15222
16029
  }
15223
16030
  return;
15224
16031
  }
@@ -15251,30 +16058,11 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
15251
16058
  );
15252
16059
  }
15253
16060
  if (staleResponse) {
15254
- await deliverFinalResponse({
15255
- adapter,
15256
- address: msg.address,
15257
- sessionId: binding.codepilotSessionId,
15258
- replyToMessageId: msg.messageId,
15259
- deliverResponse: deps.deliverResponse
15260
- }, staleResponse, { skipText: cardFinalized });
16061
+ await deliverFinalPayload(staleResponse, { skipText: cardFinalized });
15261
16062
  } else if (hasFinalResponsePayload(effectiveResponse)) {
15262
- await deliverFinalResponse({
15263
- adapter,
15264
- address: msg.address,
15265
- sessionId: binding.codepilotSessionId,
15266
- replyToMessageId: msg.messageId,
15267
- deliverResponse: deps.deliverResponse
15268
- }, effectiveResponse, { skipText: cardFinalized });
16063
+ await deliverFinalPayload(effectiveResponse, { skipText: cardFinalized });
15269
16064
  } else if (result.hasError && !taskAbort.signal.aborted) {
15270
- await deliverFinalResponse(
15271
- {
15272
- adapter,
15273
- address: msg.address,
15274
- sessionId: binding.codepilotSessionId,
15275
- replyToMessageId: msg.messageId,
15276
- deliverResponse: deps.deliverResponse
15277
- },
16065
+ await deliverFinalPayload(
15278
16066
  assembleSdkFinalResponse({
15279
16067
  text: `**Error:** ${result.errorMessage}`,
15280
16068
  hasError: true,
@@ -15286,8 +16074,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
15286
16074
  deps.persistSdkSessionUpdate(binding.codepilotSessionId, result.sdkSessionId, result.hasError);
15287
16075
  } catch {
15288
16076
  }
15289
- finalOutcome = terminalAfterProcess?.outcome || (result.hasError ? "failed" : "completed");
15290
- finalOutcomeDetail = terminalAfterProcess?.detail || (result.hasError ? result.errorMessage?.trim() || void 0 : void 0);
16077
+ if (!finalDeliveryError) {
16078
+ finalOutcome = terminalAfterProcess?.outcome || (result.hasError ? "failed" : "completed");
16079
+ finalOutcomeDetail = terminalAfterProcess?.detail || (result.hasError ? result.errorMessage?.trim() || void 0 : void 0);
16080
+ }
15291
16081
  } finally {
15292
16082
  await finalizeStreamUiOnce(
15293
16083
  taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
@@ -15484,7 +16274,7 @@ function createInteractiveRuntime(getState2, deps) {
15484
16274
  }
15485
16275
 
15486
16276
  // src/lib/bridge/mirror-runtime.ts
15487
- import fs10 from "node:fs";
16277
+ import fs11 from "node:fs";
15488
16278
 
15489
16279
  // src/lib/bridge/mirror-subscription-state.ts
15490
16280
  function resetMirrorReadState(subscription) {
@@ -15505,7 +16295,7 @@ function createMirrorSubscription(input) {
15505
16295
  chatId: input.chatId,
15506
16296
  threadId: input.threadId,
15507
16297
  filePath: input.filePath,
15508
- cursor: { initialized: false, lastEventCount: 0 },
16298
+ cursor: input.lastDeliveredAt ? { initialized: true, lastEventTimestamp: input.lastDeliveredAt, lastEventCount: 0 } : { initialized: false, lastEventCount: 0 },
15509
16299
  dirty: true,
15510
16300
  status: input.filePath ? "watching" : "stale",
15511
16301
  watcher: null,
@@ -15529,7 +16319,7 @@ function createMirrorSubscription(input) {
15529
16319
  };
15530
16320
  }
15531
16321
  function resetMirrorSubscriptionForThreadChange(subscription, lastDeliveredAt) {
15532
- subscription.cursor = { initialized: false, lastEventCount: 0 };
16322
+ subscription.cursor = lastDeliveredAt ? { initialized: true, lastEventTimestamp: lastDeliveredAt, lastEventCount: 0 } : { initialized: false, lastEventCount: 0 };
15533
16323
  subscription.lastDeliveredAt = lastDeliveredAt;
15534
16324
  subscription.dirty = true;
15535
16325
  subscription.pendingTurn = null;
@@ -15586,7 +16376,7 @@ function recordMirrorSubscriptionFailure(subscription, suspendThreshold, suspend
15586
16376
  }
15587
16377
 
15588
16378
  // src/lib/bridge/mirror-reconcile-core.ts
15589
- import fs9 from "node:fs";
16379
+ import fs10 from "node:fs";
15590
16380
 
15591
16381
  // src/desktop-session-mirror.ts
15592
16382
  function makeCursor(records) {
@@ -15677,7 +16467,7 @@ function reconcileDesktopMirrorCursor(cursor, records) {
15677
16467
  if (cursor.lastEventCount === 0) {
15678
16468
  return {
15679
16469
  nextCursor,
15680
- deliverableRecords: records,
16470
+ deliverableRecords: cursor.lastEventTimestamp ? collectEventsAfterTimestamp(records, cursor.lastEventTimestamp) : records,
15681
16471
  reset: false
15682
16472
  };
15683
16473
  }
@@ -15699,7 +16489,7 @@ function reconcileDesktopMirrorCursor(cursor, records) {
15699
16489
  // src/lib/bridge/mirror-reconcile-core.ts
15700
16490
  function statMirrorFile(filePath) {
15701
16491
  try {
15702
- const stat = fs9.statSync(filePath);
16492
+ const stat = fs10.statSync(filePath);
15703
16493
  if (!stat.isFile()) return null;
15704
16494
  return {
15705
16495
  size: stat.size,
@@ -15727,12 +16517,43 @@ function markMirrorSnapshotMissing(subscription) {
15727
16517
  resetMirrorReadState(subscription);
15728
16518
  }
15729
16519
  function isMirrorSnapshotUnchanged(subscription, snapshot) {
15730
- return !subscription.dirty && subscription.fileIdentity === snapshot.identity && subscription.fileSize === snapshot.size && subscription.fileMtimeMs === snapshot.mtimeMs;
16520
+ return !subscription.dirty && subscription.fileIdentity === snapshot.identity && subscription.fileOffset === snapshot.size && !subscription.trailingText && subscription.fileSize === snapshot.size && subscription.fileMtimeMs === snapshot.mtimeMs;
16521
+ }
16522
+ function findLastCompleteLineOffset(filePath, fileSize) {
16523
+ if (fileSize <= 0) return 0;
16524
+ const fd = fs10.openSync(filePath, "r");
16525
+ try {
16526
+ const chunkSize = 64 * 1024;
16527
+ let end = fileSize;
16528
+ while (end > 0) {
16529
+ const start2 = Math.max(0, end - chunkSize);
16530
+ const buffer = Buffer.alloc(end - start2);
16531
+ const bytesRead = fs10.readSync(fd, buffer, 0, buffer.length, start2);
16532
+ const newlineIndex = buffer.subarray(0, bytesRead).lastIndexOf(10);
16533
+ if (newlineIndex >= 0) return start2 + newlineIndex + 1;
16534
+ end = start2;
16535
+ }
16536
+ return 0;
16537
+ } finally {
16538
+ fs10.closeSync(fd);
16539
+ }
15731
16540
  }
15732
16541
  function readMirrorDeliverableRecords(subscription, snapshot) {
15733
16542
  let deliverableRecords = [];
15734
16543
  let unknownKinds = [];
15735
16544
  const requiresFullRecover = !subscription.cursor.initialized || subscription.fileOffset === 0 || subscription.fileIdentity !== null && subscription.fileIdentity !== snapshot.identity || subscription.fileSize !== null && snapshot.size < subscription.fileOffset || subscription.fileSize !== null && snapshot.size === subscription.fileOffset && subscription.fileMtimeMs !== null && snapshot.mtimeMs !== subscription.fileMtimeMs;
16545
+ if (requiresFullRecover && !subscription.cursor.initialized && !subscription.lastDeliveredAt) {
16546
+ subscription.cursor = { initialized: true, lastEventCount: 0 };
16547
+ subscription.trailingText = "";
16548
+ subscription.fileOffset = findLastCompleteLineOffset(subscription.filePath, snapshot.size);
16549
+ subscription.activeMirrorTurnId = null;
16550
+ subscription.activeSpecialCallIds.clear();
16551
+ subscription.fileSize = snapshot.size;
16552
+ subscription.fileMtimeMs = snapshot.mtimeMs;
16553
+ subscription.fileIdentity = snapshot.identity;
16554
+ subscription.dirty = false;
16555
+ return { records: [], unknownKinds: [] };
16556
+ }
15736
16557
  if (requiresFullRecover) {
15737
16558
  const previousCursor = subscription.cursor;
15738
16559
  const fullDelta = readDesktopSessionMirrorRecordDeltaByFilePath(
@@ -15746,8 +16567,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
15746
16567
  const delta = reconcileDesktopMirrorCursor(subscription.cursor, fullDelta.records);
15747
16568
  subscription.cursor = delta.nextCursor;
15748
16569
  deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.deliverableRecords);
15749
- subscription.trailingText = "";
15750
- subscription.fileOffset = snapshot.size;
16570
+ subscription.trailingText = fullDelta.trailingText;
16571
+ subscription.fileOffset = fullDelta.nextOffset;
15751
16572
  subscription.activeMirrorTurnId = fullDelta.nextTurnId;
15752
16573
  subscription.activeSpecialCallIds = new Set(fullDelta.nextSpecialCallIds);
15753
16574
  unknownKinds = fullDelta.unknownKinds;
@@ -15887,7 +16708,7 @@ function createMirrorRuntime(getState2, options, deps) {
15887
16708
  }
15888
16709
  closeMirrorWatcher(subscription);
15889
16710
  try {
15890
- subscription.watcher = fs10.watch(filePath, () => {
16711
+ subscription.watcher = fs11.watch(filePath, () => {
15891
16712
  subscription.dirty = true;
15892
16713
  scheduleMirrorWake();
15893
16714
  });
@@ -16288,7 +17109,9 @@ function createMirrorFeedbackController(deps) {
16288
17109
  }
16289
17110
  async function deliverMirrorTurn(subscription, turn) {
16290
17111
  const adapter = deps.getAdapter(subscription.channelType);
16291
- if (!adapter || !adapter.isRunning()) return;
17112
+ if (!adapter || !adapter.isRunning()) {
17113
+ throw new Error(`mirror adapter unavailable: ${subscription.channelType}`);
17114
+ }
16292
17115
  const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
16293
17116
  const responseParseMode = getFeedbackParseMode(subscription.channelType);
16294
17117
  const markdown = responseParseMode === "Markdown";
@@ -17577,10 +18400,26 @@ async function handleMessage(adapter, msg) {
17577
18400
  const meta = adapterState.adapterMeta.get(adapter.channelType) || { lastMessageAt: null, lastError: null, configFingerprint: "" };
17578
18401
  meta.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString();
17579
18402
  adapterState.adapterMeta.set(adapter.channelType, meta);
18403
+ let updateSettled = false;
18404
+ const reject = () => {
18405
+ if (updateSettled) return;
18406
+ updateSettled = true;
18407
+ if (msg.updateId != null) {
18408
+ adapter.rejectUpdate?.(msg.updateId, msg.messageId);
18409
+ }
18410
+ };
17580
18411
  const ack = () => {
17581
- if (msg.updateId != null && adapter.acknowledgeUpdate) {
17582
- adapter.acknowledgeUpdate(msg.updateId);
18412
+ if (updateSettled) return;
18413
+ if (msg.updateId != null) {
18414
+ try {
18415
+ store.insertDedup(buildInboundDedupKey(adapter.channelType, msg.messageId));
18416
+ } catch (error) {
18417
+ reject();
18418
+ throw error;
18419
+ }
18420
+ adapter.acknowledgeUpdate?.(msg.updateId, msg.messageId);
17583
18421
  }
18422
+ updateSettled = true;
17584
18423
  };
17585
18424
  if (msg.callbackData) {
17586
18425
  const handled = handlePermissionCallback(msg.callbackData, msg.address.chatId, msg.callbackMessageId);
@@ -17678,34 +18517,31 @@ async function handleMessage(adapter, msg) {
17678
18517
  ack();
17679
18518
  return;
17680
18519
  }
17681
- try {
17682
- await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
17683
- registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
17684
- registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
17685
- resetMirrorSessionForInteractiveRun,
17686
- isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
17687
- touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
17688
- recordInteractiveHealthStart: (sessionId, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveStart(sessionId, detail),
17689
- recordInteractiveHealthProgress: (sessionId, type, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveProgress(sessionId, type, detail),
17690
- recordInteractiveHealthTool: (sessionId, toolId, toolName, status) => {
17691
- SESSION_HEALTH_RUNTIME.recordToolState(sessionId, toolId, toolName, status);
17692
- },
17693
- recordInteractiveStreamUiSnapshot: (sessionId, snapshot) => {
17694
- SESSION_HEALTH_RUNTIME.recordStructuredStreamUi(sessionId, snapshot);
17695
- },
17696
- recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
17697
- beginMirrorSuppression: beginMirrorSuppression2,
17698
- abortMirrorSuppression: abortMirrorSuppression2,
17699
- settleMirrorSuppression: settleMirrorSuppression2,
17700
- releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
17701
- releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
17702
- deliverResponse,
17703
- persistSdkSessionUpdate,
17704
- desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
17705
- });
17706
- } finally {
17707
- ack();
17708
- }
18520
+ await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
18521
+ registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
18522
+ registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
18523
+ resetMirrorSessionForInteractiveRun,
18524
+ isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
18525
+ touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
18526
+ recordInteractiveHealthStart: (sessionId, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveStart(sessionId, detail),
18527
+ recordInteractiveHealthProgress: (sessionId, type, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveProgress(sessionId, type, detail),
18528
+ recordInteractiveHealthTool: (sessionId, toolId, toolName, status) => {
18529
+ SESSION_HEALTH_RUNTIME.recordToolState(sessionId, toolId, toolName, status);
18530
+ },
18531
+ recordInteractiveStreamUiSnapshot: (sessionId, snapshot) => {
18532
+ SESSION_HEALTH_RUNTIME.recordStructuredStreamUi(sessionId, snapshot);
18533
+ },
18534
+ recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
18535
+ beginMirrorSuppression: beginMirrorSuppression2,
18536
+ abortMirrorSuppression: abortMirrorSuppression2,
18537
+ settleMirrorSuppression: settleMirrorSuppression2,
18538
+ releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
18539
+ releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
18540
+ deliverResponse,
18541
+ persistSdkSessionUpdate,
18542
+ desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
18543
+ });
18544
+ ack();
17709
18545
  }
17710
18546
  async function handleCommand(adapter, msg, text2) {
17711
18547
  await handleBridgeCommand(adapter, msg, text2, {
@@ -17743,32 +18579,51 @@ function persistSdkSessionUpdate(sessionId, sdkSessionId, hasError) {
17743
18579
  }
17744
18580
 
17745
18581
  // src/store.ts
17746
- import fs11 from "node:fs";
18582
+ import fs12 from "node:fs";
17747
18583
  import path14 from "node:path";
17748
- import crypto6 from "node:crypto";
18584
+ import crypto9 from "node:crypto";
17749
18585
  var DATA_DIR2 = path14.join(CTI_HOME, "data");
17750
18586
  var MESSAGES_DIR = path14.join(DATA_DIR2, "messages");
18587
+ var SESSIONS_PATH = path14.join(DATA_DIR2, "sessions.json");
18588
+ var BINDINGS_PATH = path14.join(DATA_DIR2, "bindings.json");
18589
+ var PERMISSIONS_PATH = path14.join(DATA_DIR2, "permissions.json");
18590
+ var OFFSETS_PATH = path14.join(DATA_DIR2, "offsets.json");
18591
+ var DEDUP_PATH = path14.join(DATA_DIR2, "dedup.json");
18592
+ var AUDIT_PATH = path14.join(DATA_DIR2, "audit.json");
18593
+ var DEFAULT_DEDUP_TTL_MS = 5 * 60 * 1e3;
18594
+ var INBOUND_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
17751
18595
  function ensureDir2(dir) {
17752
- fs11.mkdirSync(dir, { recursive: true });
18596
+ fs12.mkdirSync(dir, { recursive: true });
17753
18597
  }
17754
18598
  function atomicWrite2(filePath, data) {
17755
- const tmp = filePath + ".tmp";
17756
- fs11.writeFileSync(tmp, data, "utf-8");
17757
- fs11.renameSync(tmp, filePath);
18599
+ const tmp = `${filePath}.${process.pid}.${crypto9.randomUUID()}.tmp`;
18600
+ try {
18601
+ fs12.writeFileSync(tmp, data, "utf-8");
18602
+ fs12.renameSync(tmp, filePath);
18603
+ } finally {
18604
+ try {
18605
+ fs12.rmSync(tmp, { force: true });
18606
+ } catch {
18607
+ }
18608
+ }
18609
+ }
18610
+ function dedupTtlMs(key) {
18611
+ return key.startsWith("inbound:") ? INBOUND_DEDUP_TTL_MS : DEFAULT_DEDUP_TTL_MS;
17758
18612
  }
17759
18613
  function readJson2(filePath, fallback) {
17760
18614
  try {
17761
- const raw = fs11.readFileSync(filePath, "utf-8");
18615
+ const raw = fs12.readFileSync(filePath, "utf-8");
17762
18616
  return JSON.parse(raw);
17763
- } catch {
17764
- return fallback;
18617
+ } catch (error) {
18618
+ if (error.code === "ENOENT") return fallback;
18619
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6 JSON \u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
17765
18620
  }
17766
18621
  }
17767
18622
  function writeJson(filePath, data) {
17768
18623
  atomicWrite2(filePath, JSON.stringify(data, null, 2));
17769
18624
  }
17770
18625
  function uuid() {
17771
- return crypto6.randomUUID();
18626
+ return crypto9.randomUUID();
17772
18627
  }
17773
18628
  function now() {
17774
18629
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -17827,38 +18682,38 @@ var JsonFileStore = class {
17827
18682
  this.reloadSessions();
17828
18683
  this.reloadBindings();
17829
18684
  const perms = readJson2(
17830
- path14.join(DATA_DIR2, "permissions.json"),
18685
+ PERMISSIONS_PATH,
17831
18686
  {}
17832
18687
  );
17833
18688
  for (const [id, p] of Object.entries(perms)) {
17834
18689
  this.permissionLinks.set(id, p);
17835
18690
  }
17836
18691
  const offsets = readJson2(
17837
- path14.join(DATA_DIR2, "offsets.json"),
18692
+ OFFSETS_PATH,
17838
18693
  {}
17839
18694
  );
17840
18695
  for (const [k, v] of Object.entries(offsets)) {
17841
18696
  this.offsets.set(k, v);
17842
18697
  }
17843
18698
  const dedup = readJson2(
17844
- path14.join(DATA_DIR2, "dedup.json"),
18699
+ DEDUP_PATH,
17845
18700
  {}
17846
18701
  );
17847
18702
  for (const [k, v] of Object.entries(dedup)) {
17848
18703
  this.dedupKeys.set(k, v);
17849
18704
  }
17850
- this.auditLog = readJson2(path14.join(DATA_DIR2, "audit.json"), []);
18705
+ this.auditLog = readJson2(AUDIT_PATH, []);
17851
18706
  }
17852
18707
  reloadSessions() {
17853
18708
  const sessions = readJson2(
17854
- path14.join(DATA_DIR2, "sessions.json"),
18709
+ SESSIONS_PATH,
17855
18710
  {}
17856
18711
  );
17857
18712
  this.sessions = new Map(Object.entries(sessions));
17858
18713
  }
17859
18714
  reloadBindings() {
17860
18715
  const bindings = readJson2(
17861
- path14.join(DATA_DIR2, "bindings.json"),
18716
+ BINDINGS_PATH,
17862
18717
  {}
17863
18718
  );
17864
18719
  const normalized = /* @__PURE__ */ new Map();
@@ -17872,41 +18727,62 @@ var JsonFileStore = class {
17872
18727
  }
17873
18728
  this.bindings = normalized;
17874
18729
  if (changed) {
17875
- this.persistBindings();
18730
+ withFileLock(BINDINGS_PATH, () => {
18731
+ const latest = readJson2(BINDINGS_PATH, {});
18732
+ this.bindings = new Map(Object.values(latest).map((binding) => {
18733
+ const upgraded = upgradeLegacyBinding(binding);
18734
+ return [`${upgraded.channelType}:${upgraded.chatId}`, upgraded];
18735
+ }));
18736
+ this.persistBindings();
18737
+ });
17876
18738
  }
17877
18739
  }
18740
+ reloadPermissions() {
18741
+ this.permissionLinks = new Map(Object.entries(
18742
+ readJson2(PERMISSIONS_PATH, {})
18743
+ ));
18744
+ }
18745
+ reloadOffsets() {
18746
+ this.offsets = new Map(Object.entries(readJson2(OFFSETS_PATH, {})));
18747
+ }
18748
+ reloadDedup() {
18749
+ this.dedupKeys = new Map(Object.entries(readJson2(DEDUP_PATH, {})));
18750
+ }
18751
+ reloadAudit() {
18752
+ this.auditLog = readJson2(AUDIT_PATH, []);
18753
+ }
17878
18754
  persistSessions() {
17879
18755
  writeJson(
17880
- path14.join(DATA_DIR2, "sessions.json"),
18756
+ SESSIONS_PATH,
17881
18757
  Object.fromEntries(this.sessions)
17882
18758
  );
17883
18759
  }
17884
18760
  persistBindings() {
17885
18761
  writeJson(
17886
- path14.join(DATA_DIR2, "bindings.json"),
18762
+ BINDINGS_PATH,
17887
18763
  Object.fromEntries(this.bindings)
17888
18764
  );
17889
18765
  }
17890
18766
  persistPermissions() {
17891
18767
  writeJson(
17892
- path14.join(DATA_DIR2, "permissions.json"),
18768
+ PERMISSIONS_PATH,
17893
18769
  Object.fromEntries(this.permissionLinks)
17894
18770
  );
17895
18771
  }
17896
18772
  persistOffsets() {
17897
18773
  writeJson(
17898
- path14.join(DATA_DIR2, "offsets.json"),
18774
+ OFFSETS_PATH,
17899
18775
  Object.fromEntries(this.offsets)
17900
18776
  );
17901
18777
  }
17902
18778
  persistDedup() {
17903
18779
  writeJson(
17904
- path14.join(DATA_DIR2, "dedup.json"),
18780
+ DEDUP_PATH,
17905
18781
  Object.fromEntries(this.dedupKeys)
17906
18782
  );
17907
18783
  }
17908
18784
  persistAudit() {
17909
- writeJson(path14.join(DATA_DIR2, "audit.json"), this.auditLog);
18785
+ writeJson(AUDIT_PATH, this.auditLog);
17910
18786
  }
17911
18787
  persistMessages(sessionId) {
17912
18788
  const msgs = this.messages.get(sessionId) || [];
@@ -17923,6 +18799,32 @@ var JsonFileStore = class {
17923
18799
  this.messages.set(sessionId, msgs);
17924
18800
  return msgs;
17925
18801
  }
18802
+ mutateBindings(mutation) {
18803
+ return withFileLock(BINDINGS_PATH, () => {
18804
+ this.reloadBindings();
18805
+ const result = mutation();
18806
+ this.persistBindings();
18807
+ return result;
18808
+ });
18809
+ }
18810
+ mutateSessions(mutation) {
18811
+ return withFileLock(SESSIONS_PATH, () => {
18812
+ this.reloadSessions();
18813
+ const result = mutation();
18814
+ this.persistSessions();
18815
+ return result;
18816
+ });
18817
+ }
18818
+ mutateMessages(sessionId, mutation) {
18819
+ const messagePath = path14.join(MESSAGES_DIR, `${sessionId}.json`);
18820
+ return withFileLock(messagePath, () => {
18821
+ const messages = readJson2(messagePath, []);
18822
+ this.messages.set(sessionId, messages);
18823
+ const result = mutation(messages);
18824
+ this.persistMessages(sessionId);
18825
+ return result;
18826
+ });
18827
+ }
17926
18828
  // ── Settings ──
17927
18829
  refreshSettings() {
17928
18830
  if (!this.dynamicSettings) return;
@@ -17945,66 +18847,65 @@ var JsonFileStore = class {
17945
18847
  return this.bindings.get(`${channelType}:${chatId}`) ?? null;
17946
18848
  }
17947
18849
  upsertChannelBinding(data) {
17948
- this.reloadBindings();
17949
- const key = `${data.channelType}:${data.chatId}`;
17950
- const existing = this.bindings.get(key);
17951
- if (existing) {
17952
- const updated = {
17953
- ...existing,
18850
+ return this.mutateBindings(() => {
18851
+ const key = `${data.channelType}:${data.chatId}`;
18852
+ const existing = this.bindings.get(key);
18853
+ if (existing) {
18854
+ const updated = {
18855
+ ...existing,
18856
+ codepilotSessionId: data.codepilotSessionId,
18857
+ sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
18858
+ channelProvider: data.channelProvider ?? existing.channelProvider,
18859
+ channelAlias: data.channelAlias ?? existing.channelAlias,
18860
+ chatUserId: data.chatUserId ?? existing.chatUserId,
18861
+ chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
18862
+ workingDirectory: data.workingDirectory,
18863
+ model: data.model,
18864
+ mode: data.mode ?? existing.mode,
18865
+ updatedAt: now()
18866
+ };
18867
+ this.bindings.set(key, updated);
18868
+ return updated;
18869
+ }
18870
+ const binding = {
18871
+ id: uuid(),
18872
+ channelType: data.channelType,
18873
+ channelProvider: data.channelProvider,
18874
+ channelAlias: data.channelAlias,
18875
+ chatId: data.chatId,
18876
+ chatUserId: data.chatUserId,
18877
+ chatDisplayName: data.chatDisplayName,
17954
18878
  codepilotSessionId: data.codepilotSessionId,
17955
- sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
17956
- channelProvider: data.channelProvider ?? existing.channelProvider,
17957
- channelAlias: data.channelAlias ?? existing.channelAlias,
17958
- chatUserId: data.chatUserId ?? existing.chatUserId,
17959
- chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
18879
+ sdkSessionId: data.sdkSessionId ?? "",
17960
18880
  workingDirectory: data.workingDirectory,
17961
18881
  model: data.model,
17962
- mode: data.mode ?? existing.mode,
18882
+ mode: data.mode || this.getSetting("bridge_default_mode") || "code",
18883
+ active: true,
18884
+ createdAt: now(),
17963
18885
  updatedAt: now()
17964
18886
  };
17965
- this.bindings.set(key, updated);
17966
- this.persistBindings();
17967
- return updated;
17968
- }
17969
- const binding = {
17970
- id: uuid(),
17971
- channelType: data.channelType,
17972
- channelProvider: data.channelProvider,
17973
- channelAlias: data.channelAlias,
17974
- chatId: data.chatId,
17975
- chatUserId: data.chatUserId,
17976
- chatDisplayName: data.chatDisplayName,
17977
- codepilotSessionId: data.codepilotSessionId,
17978
- sdkSessionId: data.sdkSessionId ?? "",
17979
- workingDirectory: data.workingDirectory,
17980
- model: data.model,
17981
- mode: data.mode || this.getSetting("bridge_default_mode") || "code",
17982
- active: true,
17983
- createdAt: now(),
17984
- updatedAt: now()
17985
- };
17986
- this.bindings.set(key, binding);
17987
- this.persistBindings();
17988
- return binding;
18887
+ this.bindings.set(key, binding);
18888
+ return binding;
18889
+ });
17989
18890
  }
17990
18891
  deleteChannelBinding(id) {
17991
- this.reloadBindings();
17992
- for (const [key, binding] of this.bindings) {
17993
- if (binding.id !== id) continue;
17994
- this.bindings.delete(key);
17995
- this.persistBindings();
17996
- return;
17997
- }
18892
+ this.mutateBindings(() => {
18893
+ for (const [key, binding] of this.bindings) {
18894
+ if (binding.id !== id) continue;
18895
+ this.bindings.delete(key);
18896
+ return;
18897
+ }
18898
+ });
17998
18899
  }
17999
18900
  updateChannelBinding(id, updates) {
18000
- this.reloadBindings();
18001
- for (const [key, b] of this.bindings) {
18002
- if (b.id === id) {
18003
- this.bindings.set(key, { ...b, ...updates, updatedAt: now() });
18004
- this.persistBindings();
18005
- break;
18901
+ this.mutateBindings(() => {
18902
+ for (const [key, b] of this.bindings) {
18903
+ if (b.id === id) {
18904
+ this.bindings.set(key, { ...b, ...updates, updatedAt: now() });
18905
+ break;
18906
+ }
18006
18907
  }
18007
- }
18908
+ });
18008
18909
  }
18009
18910
  listChannelBindings(channelType) {
18010
18911
  this.reloadBindings();
@@ -18031,74 +18932,78 @@ var JsonFileStore = class {
18031
18932
  return null;
18032
18933
  }
18033
18934
  createSession(name, model, systemPrompt, cwd, mode, options) {
18034
- this.reloadSessions();
18035
- const timestamp = now();
18036
- const session = {
18037
- id: uuid(),
18038
- name,
18039
- working_directory: cwd || process.cwd(),
18040
- model,
18041
- preferred_mode: mode,
18042
- system_prompt: systemPrompt,
18043
- reasoning_effort: options?.reasoningEffort,
18044
- session_type: options?.sessionType || "normal",
18045
- hidden: options?.hidden === true,
18046
- parent_session_id: options?.parentSessionId,
18047
- expires_at: options?.expiresAt,
18048
- created_at: timestamp,
18049
- updated_at: timestamp
18050
- };
18051
- this.sessions.set(session.id, session);
18052
- this.persistSessions();
18053
- return session;
18935
+ return this.mutateSessions(() => {
18936
+ const timestamp = now();
18937
+ const session = {
18938
+ id: uuid(),
18939
+ name,
18940
+ working_directory: cwd || process.cwd(),
18941
+ model,
18942
+ preferred_mode: mode,
18943
+ system_prompt: systemPrompt,
18944
+ reasoning_effort: options?.reasoningEffort,
18945
+ session_type: options?.sessionType || "normal",
18946
+ hidden: options?.hidden === true,
18947
+ parent_session_id: options?.parentSessionId,
18948
+ expires_at: options?.expiresAt,
18949
+ created_at: timestamp,
18950
+ updated_at: timestamp
18951
+ };
18952
+ this.sessions.set(session.id, session);
18953
+ return session;
18954
+ });
18054
18955
  }
18055
18956
  updateSessionProviderId(sessionId, providerId) {
18056
- this.reloadSessions();
18057
- const s = this.sessions.get(sessionId);
18058
- if (s) {
18957
+ this.mutateSessions(() => {
18958
+ const s = this.sessions.get(sessionId);
18959
+ if (!s) return;
18059
18960
  s.provider_id = providerId;
18060
18961
  s.updated_at = now();
18061
- this.persistSessions();
18062
- }
18962
+ });
18063
18963
  }
18064
18964
  updateSession(sessionId, updates, options) {
18065
- this.reloadSessions();
18066
- const session = this.sessions.get(sessionId);
18067
- if (!session) return;
18068
- const next = {
18069
- ...session,
18070
- ...updates,
18071
- id: session.id,
18072
- updated_at: options?.touch === false ? session.updated_at : now()
18073
- };
18074
- this.sessions.set(sessionId, next);
18075
- this.persistSessions();
18965
+ this.mutateSessions(() => {
18966
+ const session = this.sessions.get(sessionId);
18967
+ if (!session) return;
18968
+ const next = {
18969
+ ...session,
18970
+ ...updates,
18971
+ id: session.id,
18972
+ updated_at: options?.touch === false ? session.updated_at : now()
18973
+ };
18974
+ this.sessions.set(sessionId, next);
18975
+ });
18076
18976
  }
18077
18977
  deleteSession(sessionId) {
18078
- this.reloadSessions();
18079
- this.reloadBindings();
18080
- this.sessions.delete(sessionId);
18081
- for (const [key, binding] of this.bindings) {
18082
- if (binding.codepilotSessionId === sessionId) {
18083
- this.bindings.delete(key);
18978
+ const messagePath = path14.join(MESSAGES_DIR, `${sessionId}.json`);
18979
+ withFileLocks([SESSIONS_PATH, BINDINGS_PATH, messagePath], () => {
18980
+ this.reloadSessions();
18981
+ this.reloadBindings();
18982
+ this.sessions.delete(sessionId);
18983
+ for (const [key, binding] of this.bindings) {
18984
+ if (binding.codepilotSessionId === sessionId) {
18985
+ this.bindings.delete(key);
18986
+ }
18084
18987
  }
18085
- }
18086
- this.messages.delete(sessionId);
18087
- try {
18088
- fs11.rmSync(path14.join(MESSAGES_DIR, `${sessionId}.json`), { force: true });
18089
- } catch {
18090
- }
18091
- this.persistSessions();
18092
- this.persistBindings();
18988
+ this.messages.delete(sessionId);
18989
+ try {
18990
+ fs12.rmSync(messagePath, { force: true });
18991
+ } catch {
18992
+ }
18993
+ this.persistSessions();
18994
+ this.persistBindings();
18995
+ });
18093
18996
  }
18094
18997
  // ── Messages ──
18095
18998
  addMessage(sessionId, role, content, _usage) {
18096
- const msgs = this.loadMessages(sessionId);
18097
- msgs.push({ role, content });
18098
- this.persistMessages(sessionId);
18999
+ this.mutateMessages(sessionId, (messages) => {
19000
+ messages.push({ role, content });
19001
+ });
18099
19002
  }
18100
19003
  getMessages(sessionId, opts) {
18101
- const msgs = this.loadMessages(sessionId);
19004
+ const messagePath = path14.join(MESSAGES_DIR, `${sessionId}.json`);
19005
+ const msgs = readJson2(messagePath, []);
19006
+ this.messages.set(sessionId, msgs);
18102
19007
  if (opts?.limit && opts.limit > 0) {
18103
19008
  return { messages: msgs.slice(-opts.limit) };
18104
19009
  }
@@ -18130,61 +19035,62 @@ var JsonFileStore = class {
18130
19035
  }
18131
19036
  }
18132
19037
  setSessionRuntimeStatus(_sessionId, _status) {
18133
- this.reloadSessions();
18134
- const session = this.sessions.get(_sessionId);
18135
- if (!session) return;
18136
- const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
18137
- let runtimeStatus;
18138
- if (_status === "running") {
18139
- runtimeStatus = queuedCount > 0 ? "queued" : "running";
18140
- } else if (_status === "idle") {
18141
- runtimeStatus = queuedCount > 0 ? "queued" : "idle";
18142
- } else {
18143
- runtimeStatus = session.runtime_status;
18144
- }
18145
- const next = {
18146
- ...session,
18147
- runtime_status: runtimeStatus,
18148
- last_runtime_update_at: now(),
18149
- updated_at: now()
18150
- };
18151
- this.sessions.set(_sessionId, next);
18152
- this.persistSessions();
19038
+ this.mutateSessions(() => {
19039
+ const session = this.sessions.get(_sessionId);
19040
+ if (!session) return;
19041
+ const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
19042
+ let runtimeStatus;
19043
+ if (_status === "running") {
19044
+ runtimeStatus = queuedCount > 0 ? "queued" : "running";
19045
+ } else if (_status === "idle") {
19046
+ runtimeStatus = queuedCount > 0 ? "queued" : "idle";
19047
+ } else {
19048
+ runtimeStatus = session.runtime_status;
19049
+ }
19050
+ const next = {
19051
+ ...session,
19052
+ runtime_status: runtimeStatus,
19053
+ last_runtime_update_at: now(),
19054
+ updated_at: now()
19055
+ };
19056
+ this.sessions.set(_sessionId, next);
19057
+ });
18153
19058
  }
18154
19059
  // ── SDK Session ──
18155
19060
  updateSdkSessionId(sessionId, sdkSessionId) {
18156
- this.reloadSessions();
18157
- this.reloadBindings();
18158
- const s = this.sessions.get(sessionId);
18159
- if (s) {
18160
- s.sdk_session_id = sdkSessionId;
18161
- if (sdkSessionId) {
18162
- s.codex_thread_id = sdkSessionId;
18163
- s.thread_origin = s.thread_origin || "bridge";
18164
- } else {
18165
- delete s.codex_thread_id;
18166
- if (s.thread_origin !== "desktop") {
18167
- delete s.thread_origin;
19061
+ withFileLocks([SESSIONS_PATH, BINDINGS_PATH], () => {
19062
+ this.reloadSessions();
19063
+ this.reloadBindings();
19064
+ const s = this.sessions.get(sessionId);
19065
+ if (s) {
19066
+ s.sdk_session_id = sdkSessionId;
19067
+ if (sdkSessionId) {
19068
+ s.codex_thread_id = sdkSessionId;
19069
+ s.thread_origin = s.thread_origin || "bridge";
19070
+ } else {
19071
+ delete s.codex_thread_id;
19072
+ if (s.thread_origin !== "desktop") {
19073
+ delete s.thread_origin;
19074
+ }
18168
19075
  }
19076
+ s.updated_at = now();
18169
19077
  }
18170
- s.updated_at = now();
18171
- this.persistSessions();
18172
- }
18173
- for (const [key, b] of this.bindings) {
18174
- if (b.codepilotSessionId === sessionId) {
18175
- this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
19078
+ for (const [key, b] of this.bindings) {
19079
+ if (b.codepilotSessionId === sessionId) {
19080
+ this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
19081
+ }
18176
19082
  }
18177
- }
18178
- this.persistBindings();
19083
+ this.persistSessions();
19084
+ this.persistBindings();
19085
+ });
18179
19086
  }
18180
19087
  updateSessionModel(sessionId, model) {
18181
- this.reloadSessions();
18182
- const s = this.sessions.get(sessionId);
18183
- if (s) {
19088
+ this.mutateSessions(() => {
19089
+ const s = this.sessions.get(sessionId);
19090
+ if (!s) return;
18184
19091
  s.model = model;
18185
19092
  s.updated_at = now();
18186
- this.persistSessions();
18187
- }
19093
+ });
18188
19094
  }
18189
19095
  syncSdkTasks(_sessionId, _todos) {
18190
19096
  }
@@ -18197,66 +19103,84 @@ var JsonFileStore = class {
18197
19103
  }
18198
19104
  // ── Audit & Dedup ──
18199
19105
  insertAuditLog(entry) {
18200
- this.auditLog.push({
18201
- ...entry,
18202
- id: uuid(),
18203
- createdAt: now()
19106
+ withFileLock(AUDIT_PATH, () => {
19107
+ this.reloadAudit();
19108
+ this.auditLog.push({
19109
+ ...entry,
19110
+ id: uuid(),
19111
+ createdAt: now()
19112
+ });
19113
+ if (this.auditLog.length > 1e3) {
19114
+ this.auditLog = this.auditLog.slice(-1e3);
19115
+ }
19116
+ this.persistAudit();
18204
19117
  });
18205
- if (this.auditLog.length > 1e3) {
18206
- this.auditLog = this.auditLog.slice(-1e3);
18207
- }
18208
- this.persistAudit();
18209
19118
  }
18210
19119
  checkDedup(key) {
19120
+ this.reloadDedup();
18211
19121
  const ts = this.dedupKeys.get(key);
18212
19122
  if (ts === void 0) return false;
18213
- if (Date.now() - ts > 5 * 60 * 1e3) {
19123
+ if (Date.now() - ts > dedupTtlMs(key)) {
18214
19124
  this.dedupKeys.delete(key);
18215
19125
  return false;
18216
19126
  }
18217
19127
  return true;
18218
19128
  }
18219
19129
  insertDedup(key) {
18220
- this.dedupKeys.set(key, Date.now());
18221
- this.persistDedup();
19130
+ withFileLock(DEDUP_PATH, () => {
19131
+ this.reloadDedup();
19132
+ this.dedupKeys.set(key, Date.now());
19133
+ this.persistDedup();
19134
+ });
18222
19135
  }
18223
19136
  cleanupExpiredDedup() {
18224
- const cutoff = Date.now() - 5 * 60 * 1e3;
18225
- let changed = false;
18226
- for (const [key, ts] of this.dedupKeys) {
18227
- if (ts < cutoff) {
18228
- this.dedupKeys.delete(key);
18229
- changed = true;
19137
+ withFileLock(DEDUP_PATH, () => {
19138
+ this.reloadDedup();
19139
+ const timestamp = Date.now();
19140
+ let changed = false;
19141
+ for (const [key, ts] of this.dedupKeys) {
19142
+ if (timestamp - ts > dedupTtlMs(key)) {
19143
+ this.dedupKeys.delete(key);
19144
+ changed = true;
19145
+ }
18230
19146
  }
18231
- }
18232
- if (changed) this.persistDedup();
19147
+ if (changed) this.persistDedup();
19148
+ });
18233
19149
  }
18234
19150
  insertOutboundRef(_ref) {
18235
19151
  }
18236
19152
  // ── Permission Links ──
18237
19153
  insertPermissionLink(link2) {
18238
- const record = {
18239
- permissionRequestId: link2.permissionRequestId,
18240
- chatId: link2.chatId,
18241
- messageId: link2.messageId,
18242
- sessionId: link2.sessionId,
18243
- resolved: false,
18244
- suggestions: link2.suggestions
18245
- };
18246
- this.permissionLinks.set(link2.permissionRequestId, record);
18247
- this.persistPermissions();
19154
+ withFileLock(PERMISSIONS_PATH, () => {
19155
+ this.reloadPermissions();
19156
+ const record = {
19157
+ permissionRequestId: link2.permissionRequestId,
19158
+ chatId: link2.chatId,
19159
+ messageId: link2.messageId,
19160
+ sessionId: link2.sessionId,
19161
+ resolved: false,
19162
+ suggestions: link2.suggestions
19163
+ };
19164
+ this.permissionLinks.set(link2.permissionRequestId, record);
19165
+ this.persistPermissions();
19166
+ });
18248
19167
  }
18249
19168
  getPermissionLink(permissionRequestId) {
19169
+ this.reloadPermissions();
18250
19170
  return this.permissionLinks.get(permissionRequestId) ?? null;
18251
19171
  }
18252
19172
  markPermissionLinkResolved(permissionRequestId) {
18253
- const link2 = this.permissionLinks.get(permissionRequestId);
18254
- if (!link2 || link2.resolved) return false;
18255
- link2.resolved = true;
18256
- this.persistPermissions();
18257
- return true;
19173
+ return withFileLock(PERMISSIONS_PATH, () => {
19174
+ this.reloadPermissions();
19175
+ const link2 = this.permissionLinks.get(permissionRequestId);
19176
+ if (!link2 || link2.resolved) return false;
19177
+ link2.resolved = true;
19178
+ this.persistPermissions();
19179
+ return true;
19180
+ });
18258
19181
  }
18259
19182
  listPendingPermissionLinksByChat(chatId) {
19183
+ this.reloadPermissions();
18260
19184
  const result = [];
18261
19185
  for (const link2 of this.permissionLinks.values()) {
18262
19186
  if (link2.chatId === chatId && !link2.resolved) {
@@ -18267,11 +19191,15 @@ var JsonFileStore = class {
18267
19191
  }
18268
19192
  // ── Channel Offsets ──
18269
19193
  getChannelOffset(key) {
19194
+ this.reloadOffsets();
18270
19195
  return this.offsets.get(key) ?? "0";
18271
19196
  }
18272
19197
  setChannelOffset(key, offset) {
18273
- this.offsets.set(key, offset);
18274
- this.persistOffsets();
19198
+ withFileLock(OFFSETS_PATH, () => {
19199
+ this.reloadOffsets();
19200
+ this.offsets.set(key, offset);
19201
+ this.persistOffsets();
19202
+ });
18275
19203
  }
18276
19204
  };
18277
19205
 
@@ -18314,7 +19242,7 @@ var PendingPermissions = class {
18314
19242
  };
18315
19243
 
18316
19244
  // src/logger.ts
18317
- import fs12 from "node:fs";
19245
+ import fs13 from "node:fs";
18318
19246
  import path15 from "node:path";
18319
19247
  import { inspect as inspect2 } from "node:util";
18320
19248
  var MASK_PATTERNS = [
@@ -18355,11 +19283,11 @@ function formatLogArg(value) {
18355
19283
  return String(value);
18356
19284
  }
18357
19285
  function openLogStream() {
18358
- return fs12.createWriteStream(LOG_PATH, { flags: "a" });
19286
+ return fs13.createWriteStream(LOG_PATH, { flags: "a" });
18359
19287
  }
18360
19288
  function rotateIfNeeded() {
18361
19289
  try {
18362
- const stat = fs12.statSync(LOG_PATH);
19290
+ const stat = fs13.statSync(LOG_PATH);
18363
19291
  if (stat.size < MAX_LOG_SIZE) return;
18364
19292
  } catch {
18365
19293
  return;
@@ -18369,17 +19297,17 @@ function rotateIfNeeded() {
18369
19297
  logStream = null;
18370
19298
  }
18371
19299
  const path32 = `${LOG_PATH}.${MAX_ROTATED}`;
18372
- if (fs12.existsSync(path32)) fs12.unlinkSync(path32);
19300
+ if (fs13.existsSync(path32)) fs13.unlinkSync(path32);
18373
19301
  for (let i = MAX_ROTATED - 1; i >= 1; i--) {
18374
19302
  const src = `${LOG_PATH}.${i}`;
18375
19303
  const dst = `${LOG_PATH}.${i + 1}`;
18376
- if (fs12.existsSync(src)) fs12.renameSync(src, dst);
19304
+ if (fs13.existsSync(src)) fs13.renameSync(src, dst);
18377
19305
  }
18378
- fs12.renameSync(LOG_PATH, `${LOG_PATH}.1`);
19306
+ fs13.renameSync(LOG_PATH, `${LOG_PATH}.1`);
18379
19307
  logStream = openLogStream();
18380
19308
  }
18381
19309
  function setupLogger() {
18382
- fs12.mkdirSync(LOG_DIR, { recursive: true });
19310
+ fs13.mkdirSync(LOG_DIR, { recursive: true });
18383
19311
  logStream = openLogStream();
18384
19312
  const write = (level, args) => {
18385
19313
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -18395,18 +19323,18 @@ function setupLogger() {
18395
19323
  }
18396
19324
 
18397
19325
  // src/bridge-instance-lock.ts
18398
- import fs13 from "node:fs";
19326
+ import fs14 from "node:fs";
18399
19327
  import path16 from "node:path";
18400
19328
  var runtimeDir = path16.join(CTI_HOME, "runtime");
18401
19329
  var bridgeInstanceLockFile = path16.join(runtimeDir, "bridge.instance.lock");
18402
19330
  function readJsonFile(filePath, fallback) {
18403
19331
  try {
18404
- return JSON.parse(fs13.readFileSync(filePath, "utf-8"));
19332
+ return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
18405
19333
  } catch {
18406
19334
  return fallback;
18407
19335
  }
18408
19336
  }
18409
- function isProcessAlive(pid) {
19337
+ function isProcessAlive2(pid) {
18410
19338
  if (!pid) return false;
18411
19339
  try {
18412
19340
  process.kill(pid, 0);
@@ -18426,15 +19354,15 @@ function tryAcquireBridgeInstanceLock(options = {}) {
18426
19354
  const filePath = options.filePath ?? bridgeInstanceLockFile;
18427
19355
  const ownerPid = options.ownerPid ?? process.pid;
18428
19356
  const nowMs = options.nowMs ?? Date.now();
18429
- const isAlive = options.isAlive ?? isProcessAlive;
19357
+ const isAlive = options.isAlive ?? isProcessAlive2;
18430
19358
  const payload = {
18431
19359
  pid: ownerPid,
18432
19360
  createdAt: new Date(nowMs).toISOString()
18433
19361
  };
18434
19362
  for (let attempt = 0; attempt < 2; attempt += 1) {
18435
19363
  try {
18436
- fs13.mkdirSync(path16.dirname(filePath), { recursive: true });
18437
- fs13.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
19364
+ fs14.mkdirSync(path16.dirname(filePath), { recursive: true });
19365
+ fs14.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
18438
19366
  return { acquired: true };
18439
19367
  } catch (error) {
18440
19368
  const code2 = error.code;
@@ -18444,7 +19372,7 @@ function tryAcquireBridgeInstanceLock(options = {}) {
18444
19372
  return { acquired: false, holderPid: existing2.pid };
18445
19373
  }
18446
19374
  try {
18447
- fs13.unlinkSync(filePath);
19375
+ fs14.unlinkSync(filePath);
18448
19376
  } catch {
18449
19377
  }
18450
19378
  }
@@ -18459,14 +19387,14 @@ function releaseBridgeInstanceLock(filePath = bridgeInstanceLockFile, ownerPid =
18459
19387
  const existing = readBridgeInstanceLock(filePath);
18460
19388
  if (!existing) {
18461
19389
  try {
18462
- fs13.unlinkSync(filePath);
19390
+ fs14.unlinkSync(filePath);
18463
19391
  } catch {
18464
19392
  }
18465
19393
  return;
18466
19394
  }
18467
19395
  if (existing.pid !== ownerPid) return;
18468
19396
  try {
18469
- fs13.unlinkSync(filePath);
19397
+ fs14.unlinkSync(filePath);
18470
19398
  } catch {
18471
19399
  }
18472
19400
  }
@@ -18480,16 +19408,16 @@ async function resolveProvider() {
18480
19408
  return new CodexProvider2();
18481
19409
  }
18482
19410
  function writeStatus(info) {
18483
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
19411
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
18484
19412
  let existing = {};
18485
19413
  try {
18486
- existing = JSON.parse(fs15.readFileSync(STATUS_FILE, "utf-8"));
19414
+ existing = JSON.parse(fs16.readFileSync(STATUS_FILE, "utf-8"));
18487
19415
  } catch {
18488
19416
  }
18489
19417
  const merged = { ...existing, ...info };
18490
19418
  const tmp = STATUS_FILE + ".tmp";
18491
- fs15.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
18492
- fs15.renameSync(tmp, STATUS_FILE);
19419
+ fs16.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
19420
+ fs16.renameSync(tmp, STATUS_FILE);
18493
19421
  }
18494
19422
  function getRunningChannels() {
18495
19423
  return getStatus().adapters.map((adapter) => adapter.channelType).sort();
@@ -18518,7 +19446,7 @@ async function main() {
18518
19446
  };
18519
19447
  const config2 = loadConfig();
18520
19448
  setupLogger();
18521
- const runId = crypto7.randomUUID();
19449
+ const runId = crypto10.randomUUID();
18522
19450
  console.log(`[codex-to-im] Starting bridge (run_id: ${runId})`);
18523
19451
  const settings = configToSettings(config2);
18524
19452
  const store = new JsonFileStore(settings, { dynamicSettings: true });
@@ -18534,8 +19462,8 @@ async function main() {
18534
19462
  permissions: gateway,
18535
19463
  lifecycle: {
18536
19464
  onBridgeStart: () => {
18537
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
18538
- fs15.writeFileSync(PID_FILE, String(process.pid), "utf-8");
19465
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
19466
+ fs16.writeFileSync(PID_FILE, String(process.pid), "utf-8");
18539
19467
  const channels = getRunningChannels();
18540
19468
  writeStatus({
18541
19469
  running: true,