aws-runtime-bridge 1.9.61 → 1.9.65

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.
@@ -42,7 +42,6 @@ export class McpServer {
42
42
  memoryStore;
43
43
  roleMemoryStore;
44
44
  sessionContextMemoryStore;
45
- memoryStoresShareFile;
46
45
  initialRoleName = "";
47
46
  /** 是否有真实的角色名(非 env 空值 fallback "default-role") */
48
47
  hasRoleName = false;
@@ -50,52 +49,16 @@ export class McpServer {
50
49
  contextManager;
51
50
  /** 子 Agent 模式:仅暴露 Memory 工具,拒绝非 Memory 工具调用 */
52
51
  isSubAgent;
53
- /**
54
- * 内存级去重缓存:记录本轮已通知过的 listable memory 文件变更状态。
55
- * key = memoryId,value = 已通知时刻各绑定文件的 MD5 复合哈希。
56
- * 文件再次变更(MD5 变化)时仍会触发新通知。
57
- * 进程重启后自动清空(届时重新检测)。
58
- */
59
- notifiedFileChangeMemoryIds = new Map();
60
- /**
61
- * Step1「memory 仍处于 updating 状态」提醒的冷却时间戳表。
62
- * key = memory URI,value = 上次发送「请继续更新」提醒的时间戳。
63
- * 用于避免 buildMemoryFileChangeNotification 在 polling 周期内对 sticky 的 updating
64
- * memory 每轮都裸发 _acodeAutoDelegate(进而导致运行层反复 spawn 相同子任务)。
65
- * 冷却窗口内不再发出该提醒;窗口到期后若 memory 仍未处理,才会重新委托。
66
- */
67
- lastUpdatePendingNotifyAt = new Map();
68
- /** Step1「仍 updating」提醒的冷却窗口(5 分钟)。冷却期内不重复裸发 _acodeAutoDelegate。 */
69
- static UPDATE_PENDING_NOTIFY_COOLDOWN_MS = 5 * 60 * 1000;
70
- /**
71
- * 本地缓存 listable 记忆(recently_memory/agent_memory)的文件 MD5 map。
72
- * 服务端(Java 后端)不持久化 file_md5_map 字段,listByType 返回的记录该字段为 undefined。
73
- * 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
74
- */
75
- listableMd5Cache = new Map();
76
52
  /**
77
53
  * 租约续期定时器 (v1.21.0)。
78
54
  * 每 60 秒续期一次当前 agent 持有的所有 updating memory 的租约。
79
55
  */
80
56
  leaseRenewalTimer = null;
57
+ activeLeaseUris = new Set();
81
58
  static LEASE_DURATION_MINUTES = 10;
82
59
  static LEASE_RENEW_INTERVAL_MS = 60_000;
83
- /**
84
- * claim 去重缓存 (v1.21.0)。
85
- * 避免 5s 后台定时器重复调 server claim 同一个 memory。
86
- * key = memory URI, value = 本地缓存的租约到期时间戳。
87
- * 仅缓存 claim 成功的 URI,在 lease 到期前不再调 server。
88
- */
89
- claimedCache = new Map();
90
60
  /** profile 是否已同步(防止 activelySyncProfile 与 profileHandler 双重同步) */
91
61
  profileSynced = false;
92
- /**
93
- * Memory 通知 null 结果缓存:key → expiresAt 时间戳。
94
- * 仅缓存 null(无通知)结果,避免 while(true) 每 30s 重复读盘。
95
- * 有通知时立即投递并清除缓存,不缓存非 null 结果。
96
- */
97
- memoryNotificationNullCache = new Map();
98
- static MEMORY_NOTIFICATION_NULL_TTL_MS = 60_000;
99
62
  /**
100
63
  * poll_message 单例控制:防止并发调用导致消息被第一个消费后第二个空等。
101
64
  * 当已有 poll 在进行时,后续调用直接等待同一个 Promise。
@@ -215,7 +178,7 @@ export class McpServer {
215
178
  }
216
179
  }
217
180
  logger.info(`[McpServer] 永久记忆同步完成: 成功 ${synced}, 跳过 ${skipped}, 共 ${localMemories.length}`);
218
- await this.reportCombinedMemoryStats();
181
+ await this.reportMemoryStatsToServer();
219
182
  }
220
183
  /**
221
184
  * 规范化绑定的文件路径。
@@ -320,7 +283,6 @@ export class McpServer {
320
283
  this.memoryStore = new ServerMemoryStore(this.agentClient);
321
284
  this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
322
285
  this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
323
- this.memoryStoresShareFile = false;
324
286
  this.isSubAgent = process.env.AWS_SUB_AGENT === "true";
325
287
  this.contextManager = new ContextManager(this.sessionContextMemoryStore);
326
288
  this.server = new Server({
@@ -622,101 +584,64 @@ export class McpServer {
622
584
  return McpServer.MEMORY_TYPES.some((memoryType) => memoryType === value);
623
585
  }
624
586
  /**
625
- * 按记忆生命周期选择存储作用域。
626
- * 主流程:长期 project_memory 走角色级 store;recently_memory/agent_memory 走当前 Agent 实例级 store。
587
+ * 所有类型的记忆统一走服务端(Server 是唯一权威源)。
588
+ * 本地 roleMemoryStore 仅用于 project_memory 的备份写入与启动恢复。
627
589
  */
628
- /**
629
- * 按记忆生命周期选择存储作用域。
630
- * 主流程:永久记忆(project_memory)始终走本地 roleMemoryStore(.agentswork),
631
- * 确保本地文件始终有副本,后续部署恢复时可通过 Local → Server 同步回 server。
632
- * recently_memory/agent_memory/session_context 始终走实例级 server store。
633
- */
634
- getMemoryStoreForType(memoryType) {
635
- if (memoryType === "project_memory") {
636
- return this.roleMemoryStore;
637
- }
590
+ getMemoryStoreForType(_memoryType) {
638
591
  return this.memoryStore;
639
592
  }
640
593
  /**
641
- * 在未显式指定类型时按两个作用域读取。
642
- * 主流程:先查角色级长期记忆 -> 再查实例级临时/近期记忆,保持旧工具参数可选语义。
594
+ * 读取记忆:直接走服务端(Server 是唯一权威源)。
643
595
  */
644
596
  async readMemoryFromScopedStores(input) {
645
- if (this.memoryStoresShareFile) {
646
- return await this.memoryStore.readMemory(input);
647
- }
648
597
  if (input.memory_type) {
649
- // project_memory:服务端为权威源(claim/租约/memory_status 均在服务端维护),
650
- // 本地 roleMemoryStore 仅作服务端不可用时的离线降级副本。
651
- // 之前本地优先会导致 read_memory 读到过时的 memory_status='normal',
652
- // 而服务端 DB 已是 'updating'(文件变更 claim 只写服务端),状态脱节。
653
- if (input.memory_type === "project_memory") {
654
- try {
655
- const serverRecord = await this.memoryStore.readMemory(input);
656
- if (serverRecord) {
657
- return serverRecord;
658
- }
659
- }
660
- catch {
661
- // 服务端不可用,降级到本地
662
- }
663
- return await this.roleMemoryStore.readMemory(input);
664
- }
665
- // recently_memory / agent_memory:实例级,走服务端
666
598
  return await this.memoryStore.readMemory(input);
667
599
  }
668
- // 未指定 memory_type:优先查服务端 project_memory(权威源),
669
- // 降级本地 roleMemoryStore(离线未同步记录),最后查服务端实例级记忆。
600
+ const longTerm = await this.memoryStore.readMemory({
601
+ ...input,
602
+ memory_type: "project_memory",
603
+ }).catch(() => null);
604
+ if (longTerm) {
605
+ return longTerm;
606
+ }
607
+ return await this.memoryStore.readMemory(input);
608
+ }
609
+ /**
610
+ * 将 project_memory 异步备份到本地文件(纯灾备,不作为读取源)。
611
+ */
612
+ async backupProjectMemoryToLocal(input) {
670
613
  try {
671
- const serverLongTerm = await this.memoryStore.readMemory({
672
- ...input,
614
+ const record = await this.memoryStore.readMemory({
615
+ uri: input.uri,
673
616
  memory_type: "project_memory",
674
617
  });
675
- if (serverLongTerm) {
676
- return serverLongTerm;
618
+ if (record) {
619
+ await this.roleMemoryStore.deleteMemory({ uri: input.uri, memory_type: "project_memory" }).catch(() => { });
620
+ await this.roleMemoryStore.createMemory(record);
677
621
  }
678
622
  }
679
- catch {
680
- // 服务端不可用,降级到本地
623
+ catch (e) {
624
+ logger.warn("[McpServer] backupProjectMemoryToLocal 失败: uri=%s", input.uri, e);
681
625
  }
682
- const localLongTerm = await this.roleMemoryStore.readMemory({
683
- ...input,
684
- memory_type: "project_memory",
685
- });
686
- if (localLongTerm) {
687
- return localLongTerm;
626
+ }
627
+ /**
628
+ * 删除 project_memory 的本地备份文件。
629
+ */
630
+ async removeProjectMemoryLocalBackup(uri) {
631
+ try {
632
+ await this.roleMemoryStore.deleteMemory({ uri, memory_type: "project_memory" });
633
+ }
634
+ catch (e) {
635
+ logger.warn("[McpServer] removeProjectMemoryLocalBackup 失败: uri=%s", uri, e);
688
636
  }
689
- return await this.memoryStore.readMemory(input);
690
637
  }
691
638
  /**
692
- * 在未显式指定类型时定位可更新/删除的存储。
693
- * 主流程:按读取优先级查找记录 -> 返回命中的 store 与规范化类型,避免跨作用域误写。
639
+ * 定位可更新/删除的存储:统一走服务端。
694
640
  */
695
641
  async findMemoryStoreForMutation(input) {
696
- if (this.memoryStoresShareFile) {
697
- return { store: this.memoryStore, memory_type: input.memory_type };
698
- }
699
642
  if (input.memory_type) {
700
- // project_memory:服务端为权威源(claim/memory_status/租约均在服务端维护),
701
- // mutation 必须走服务端,否则本地写入与服务端状态脱节。
702
- // 本地 roleMemoryStore 仅在“本地存在离线记录但服务端没有”时降级(未同步的离线编辑);
703
- // 新建(两者均无)必须落到服务端,避免 create 写入本地导致服务端统计/可见性脱节。
704
- if (input.memory_type === "project_memory") {
705
- const serverRecord = await this.memoryStore.readMemory(input).catch(() => null);
706
- if (serverRecord) {
707
- return { store: this.memoryStore, memory_type: "project_memory" };
708
- }
709
- const localRecord = await this.roleMemoryStore.readMemory(input).catch(() => null);
710
- if (localRecord) {
711
- return { store: this.roleMemoryStore, memory_type: "project_memory" };
712
- }
713
- return { store: this.memoryStore, memory_type: "project_memory" };
714
- }
715
- // recently_memory / agent_memory:实例级,走服务端
716
643
  return { store: this.memoryStore, memory_type: input.memory_type };
717
644
  }
718
- // 未指定 memory_type:优先查服务端 project_memory(权威源),
719
- // 降级本地 roleMemoryStore(离线未同步记录),最后回退服务端实例级。
720
645
  const serverLongTerm = await this.memoryStore.readMemory({
721
646
  uri: input.uri,
722
647
  memory_type: "project_memory",
@@ -724,59 +649,15 @@ export class McpServer {
724
649
  if (serverLongTerm) {
725
650
  return { store: this.memoryStore, memory_type: "project_memory" };
726
651
  }
727
- const localLongTerm = await this.roleMemoryStore.readMemory({
728
- uri: input.uri,
729
- memory_type: "project_memory",
730
- });
731
- if (localLongTerm) {
732
- return { store: this.roleMemoryStore, memory_type: "project_memory" };
733
- }
734
652
  return { store: this.memoryStore };
735
653
  }
736
- /**
737
- * 合并角色级长期记忆与实例级短期记忆容量统计。
738
- * 主流程:实例 store 负责 listable 限额;角色 store 只贡献长期 memory 数量。
739
- */
740
- combineMemoryStats(instanceStats, roleStats) {
741
- if (this.memoryStoresShareFile) {
742
- return instanceStats;
743
- }
744
- // 服务端 now 也按角色级返回永久记忆统计(同角色实例共享),
745
- // 本地 roleMemoryStore 也可能有尚未同步到服务端的记录。
746
- // permanentCount 取两者的较大值作为保守估计(下界)。
747
- // 注意: 当 instance 和 role 分别有对方没有的唯一 URI 时,实际并集 > Math.max,
748
- // 但此处只有 count 无 URI 列表,无法精确计算并集,使用下界避免面板显示虚高。
749
- const permanentCount = Math.max(instanceStats.by_type.project_memory.count, roleStats.by_type.project_memory.count);
750
- // total 同样保守估算:用 server 非 project_memory count + permanentCount 下界
751
- const serverNonMemoryTotal = instanceStats.total - instanceStats.by_type.project_memory.count;
752
- return {
753
- total: serverNonMemoryTotal + permanentCount,
754
- listable: instanceStats.listable,
755
- by_type: {
756
- recently_memory: instanceStats.by_type.recently_memory,
757
- agent_memory: instanceStats.by_type.agent_memory,
758
- project_memory: {
759
- ...roleStats.by_type.project_memory,
760
- count: permanentCount,
761
- },
762
- session_context: instanceStats.by_type.session_context,
763
- },
764
- };
765
- }
766
- /**
767
- * 上报当前合并后的记忆统计。
768
- * 主流程:读取实例级与角色级统计 -> 合并长期 memory count -> 通过 server 广播给 Dashboard。
769
- */
770
- async reportCombinedMemoryStats() {
771
- const instanceStats = await this.memoryStore.getStats();
772
- const mergedStats = this.hasRoleName
773
- ? this.combineMemoryStats(instanceStats, await this.roleMemoryStore.getStats())
774
- : instanceStats;
654
+ async reportMemoryStatsToServer() {
655
+ const stats = await this.memoryStore.getStats();
775
656
  try {
776
- await this.agentClient.reportMemoryStats(mergedStats);
657
+ await this.agentClient.reportMemoryStats(stats);
777
658
  }
778
659
  catch (error) {
779
- logger.warn("[McpServer] 上报合并 memory 统计失败,已保留本地记忆写入结果:", error);
660
+ logger.warn("[McpServer] 上报 memory 统计失败:", error);
780
661
  }
781
662
  }
782
663
  /**
@@ -884,19 +765,13 @@ export class McpServer {
884
765
  file_md5_map: fileMd5Map,
885
766
  last_check_time: lastCheckTime,
886
767
  };
887
- const created = await this.getMemoryStoreForType(input.memory_type).createMemory(input);
888
- // Dual-write permanent memory to server BEFORE reporting stats,
889
- // 确保 reportCombinedMemoryStats 上报时 server 端已包含 project_memory 记录,
890
- // 避免 Dashboard 统计与本地写入脱节。
768
+ const created = await this.memoryStore.createMemory(input);
891
769
  if (input.memory_type === "project_memory") {
892
- try {
893
- await this.memoryStore.createMemory(input);
894
- }
895
- catch (e) {
896
- logger.warn("[McpServer] Failed to sync permanent memory to server:", e);
897
- }
770
+ this.backupProjectMemoryToLocal(input).catch((e) => {
771
+ logger.warn("[McpServer] 备份 project_memory 到本地失败:", e);
772
+ });
898
773
  }
899
- await this.reportCombinedMemoryStats();
774
+ await this.reportMemoryStatsToServer();
900
775
  // v1.13.0: recently_memory 过期判定、agent_memory 过期/回忆状态由服务端 memory_status 字段管理,
901
776
  // 无需 TS 侧本地缓存。服务端默认按类型填充正确状态(recently_memory='normal', agent_memory='normal')。
902
777
  return this.toMemoryWriteResult(created);
@@ -946,20 +821,15 @@ export class McpServer {
946
821
  uri: input.uri,
947
822
  memory_type: target.memory_type ?? input.memory_type,
948
823
  });
949
- // 当记忆已绑定文件或处于更新中状态时,要求显式指定 bound_files 并校验文件存在性。
950
- // 这确保文件被删除后,Agent 必须更新 bound_files 列表(移除已删除文件)才能完成更新,
951
- // 避免已删除文件路径作为"僵尸绑定"长期保留。
952
- const hasExistingBoundFiles = !!existingForMd5?.bound_files && existingForMd5.bound_files.length > 0;
953
- const isUpdatingState = existingForMd5?.memory_status === "updating";
954
- if ((hasExistingBoundFiles || isUpdatingState) &&
955
- input.bound_files === undefined) {
956
- throw new Error("此记忆已绑定文件或处于更新中状态,更新时必须指定 bound_files 列表。" +
957
- "请提供 bound_files 参数(如需移除已删除的文件,请更新列表仅保留当前存在的文件)。");
958
- }
959
- // 校验 bound_files 中所有文件是否存在:不存在的文件会导致更新失败
824
+ // 校验绑定文件存在性:
825
+ // - 如果提供了新的 bound_files,校验新列表中的文件是否都存在
826
+ // - 如果未提供 bound_files,校验当前已绑定的文件是否都存在
960
827
  if (input.bound_files !== undefined) {
961
828
  await this.validateBoundFilesExist(input.bound_files);
962
829
  }
830
+ else if (existingForMd5?.bound_files && existingForMd5.bound_files.length > 0) {
831
+ await this.validateBoundFilesExist(existingForMd5.bound_files);
832
+ }
963
833
  const effectiveBoundFiles = input.bound_files ?? existingForMd5?.bound_files;
964
834
  // 重新计算绑定文件的 MD5 map,确保更新后 MD5 与文件实际内容一致
965
835
  let fileMd5Map;
@@ -979,31 +849,24 @@ export class McpServer {
979
849
  file_md5_map: fileMd5Map,
980
850
  last_check_time: lastCheckTime,
981
851
  });
982
- // Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
983
- // 当 target.store 已是服务端(兄弟实例创建的记忆回退场景)时跳过,避免重复写。
984
- // 必须在 reportCombinedMemoryStats 之前完成,确保上报时 server 端统计已同步。
985
- if (target.memory_type === "project_memory" && target.store !== this.memoryStore) {
986
- try {
987
- await this.memoryStore.updateMemory({
988
- ...input,
989
- memory_type: "project_memory",
990
- file_md5_map: fileMd5Map,
991
- last_check_time: lastCheckTime,
992
- });
993
- }
994
- catch (e) {
995
- logger.warn("[McpServer] Failed to sync permanent memory update to server:", e);
996
- }
852
+ if (target.memory_type === "project_memory") {
853
+ this.backupProjectMemoryToLocal({
854
+ ...input,
855
+ memory_type: "project_memory",
856
+ file_md5_map: fileMd5Map,
857
+ last_check_time: lastCheckTime,
858
+ }).catch((e) => {
859
+ logger.warn("[McpServer] 备份 project_memory 更新到本地失败:", e);
860
+ });
997
861
  }
998
- await this.reportCombinedMemoryStats();
862
+ await this.reportMemoryStatsToServer();
999
863
  // v1.13.0: agent_memory 过期与回忆状态由服务端 memory_status 字段管理。
1000
864
  // v1.21.0: 如果是 project_memory 且之前是 updating 状态,释放 Server claim
1001
865
  if (target.memory_type === 'project_memory' && existingForMd5?.memory_status === 'updating') {
1002
866
  try {
1003
867
  await this.memoryStore.releaseMemoryClaim(input.uri);
1004
- this.claimedCache.delete(input.uri);
1005
- // 所有 claim 已释放,停止续期定时器避免无意义请求
1006
- if (this.claimedCache.size === 0) {
868
+ this.activeLeaseUris.delete(input.uri);
869
+ if (this.activeLeaseUris.size === 0) {
1007
870
  this.stopLeaseRenewal();
1008
871
  }
1009
872
  }
@@ -1068,7 +931,7 @@ export class McpServer {
1068
931
  }
1069
932
  }
1070
933
  if (deletedCount > 0) {
1071
- await this.reportCombinedMemoryStats();
934
+ await this.reportMemoryStatsToServer();
1072
935
  }
1073
936
  return {
1074
937
  deleted: deletedCount,
@@ -1078,7 +941,7 @@ export class McpServer {
1078
941
  }
1079
942
  /**
1080
943
  * 删除单条记忆的内部辅助方法。
1081
- * - skipStats=true 时跳过 reportCombinedMemoryStats 调用,由批量调用方统一上报一次。
944
+ * - skipStats=true 时跳过 reportMemoryStatsToServer 调用,由批量调用方统一上报一次。
1082
945
  * - 返回 true/false 表示是否实际删除(与 store.deleteMemory 一致)。
1083
946
  */
1084
947
  async deleteMemorySingle(uri, memory_type, skipStats = false) {
@@ -1090,20 +953,13 @@ export class McpServer {
1090
953
  memory_type: effectiveType,
1091
954
  });
1092
955
  if (deleted) {
1093
- // v1.13.0: 服务端 deleteMemory 内部已处理相关清理逻辑,
1094
- // TS 侧无需任何本地元数据清理。
1095
- // Dual-write permanent memory delete to server
1096
- // 当 target.store 已是服务端时跳过,避免重复删除。
1097
- if (effectiveType === "project_memory" && target.store !== this.memoryStore) {
1098
- try {
1099
- await this.memoryStore.deleteMemory({ uri, memory_type: "project_memory" });
1100
- }
1101
- catch (e) {
1102
- logger.warn("[McpServer] Failed to sync permanent memory delete to server:", e);
1103
- }
956
+ if (effectiveType === "project_memory") {
957
+ this.removeProjectMemoryLocalBackup(uri).catch((e) => {
958
+ logger.warn("[McpServer] 删除 project_memory 本地备份失败:", e);
959
+ });
1104
960
  }
1105
961
  if (!skipStats) {
1106
- await this.reportCombinedMemoryStats();
962
+ await this.reportMemoryStatsToServer();
1107
963
  }
1108
964
  }
1109
965
  return deleted;
@@ -1148,34 +1004,7 @@ export class McpServer {
1148
1004
  * 具体逻辑:按 memory_type 分流 -> 无类型时合并 roleMemoryStore(永久)与 memoryStore(recently/agent)。
1149
1005
  */
1150
1006
  async searchOwnMemories(input) {
1151
- if (input.memory_type) {
1152
- return await this.getMemoryStoreForType(input.memory_type).searchMemory(input);
1153
- }
1154
- if (this.memoryStoresShareFile) {
1155
- return await this.memoryStore.searchMemory(input);
1156
- }
1157
- // 无角色名时不查询 roleMemoryStore(避免 default-role 共享目录导致跨角色污染)
1158
- if (!this.hasRoleName) {
1159
- return await this.memoryStore.searchMemory(input);
1160
- }
1161
- const [roleResult, instanceResult] = await Promise.all([
1162
- this.roleMemoryStore.searchMemory({ ...input, memory_type: "project_memory" }),
1163
- this.memoryStore.searchMemory(input),
1164
- ]);
1165
- const seen = new Set();
1166
- const limit = typeof input.limit === "number" && input.limit > 0 ? input.limit : 10;
1167
- const results = [...roleResult.results, ...instanceResult.results]
1168
- .filter((record) => {
1169
- const key = `${record.memory_type}:${record.uri}`;
1170
- if (seen.has(key)) {
1171
- return false;
1172
- }
1173
- seen.add(key);
1174
- return true;
1175
- })
1176
- .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
1177
- .slice(0, limit);
1178
- return { query: roleResult.query || instanceResult.query, results };
1007
+ return await this.memoryStore.searchMemory(input);
1179
1008
  }
1180
1009
  /**
1181
1010
  * 合并两组搜索结果,按 uri 去重、更新时间排序。
@@ -1214,10 +1043,7 @@ export class McpServer {
1214
1043
  * 主流程:读取 MemoryStore 统计结果,供 Agent 在创建前判断 listable/type 容量。
1215
1044
  */
1216
1045
  async handleGetMemoryStats(args) {
1217
- const instanceStats = await this.memoryStore.getStats();
1218
- const stats = this.hasRoleName
1219
- ? this.combineMemoryStats(instanceStats, await this.roleMemoryStore.getStats())
1220
- : instanceStats;
1046
+ const stats = await this.memoryStore.getStats();
1221
1047
  const memoryType = this.getOptionalMemoryType(args);
1222
1048
  if (!memoryType) {
1223
1049
  return stats;
@@ -1250,16 +1076,11 @@ export class McpServer {
1250
1076
  const rawScope = this.getOptionalString(args, "scope");
1251
1077
  // 默认 scope='all':合并搜索自身与子Agent绑定文件的记忆
1252
1078
  const scope = rawScope === "own" || rawScope === "children" ? rawScope : "all";
1253
- // 搜索子 Agent 的 memory(server + 本地文件系统双路径,并行)
1254
1079
  const fetchChildMemories = async () => {
1255
- const [serverResult, localResult] = await Promise.all([
1256
- this.memoryStore.listByFile(normalizedPath, {
1257
- memory_type: memoryType,
1258
- scope: "children",
1259
- }).catch(() => []), // server 不可用时降级
1260
- this.searchChildMemoriesByFileLocal(normalizedPath, memoryType),
1261
- ]);
1262
- return this.mergeIdDedupe(serverResult, localResult);
1080
+ return await this.memoryStore.listByFile(normalizedPath, {
1081
+ memory_type: memoryType,
1082
+ scope: "children",
1083
+ }).catch(() => []);
1263
1084
  };
1264
1085
  if (scope === "children") {
1265
1086
  const childResult = await fetchChildMemories();
@@ -1429,25 +1250,9 @@ export class McpServer {
1429
1250
  * 同时查询临时记忆(server)和永久记忆(local roleMemoryStore)。
1430
1251
  */
1431
1252
  async collectOwnMemoriesByFile(filePath, memoryType) {
1432
- const serverResult = await this.memoryStore.listByFile(filePath, {
1253
+ return await this.memoryStore.listByFile(filePath, {
1433
1254
  memory_type: memoryType,
1434
1255
  });
1435
- let localResult = [];
1436
- // 补充查询本地永久记忆(server 可能未同步);无角色名时跳过(避免 default-role 跨角色污染)
1437
- if (this.hasRoleName && (!memoryType || memoryType === "project_memory")) {
1438
- localResult = await this.roleMemoryStore.listByFile(filePath, "project_memory");
1439
- }
1440
- // 去重合并
1441
- const seen = new Set(serverResult.map(m => `${m.memory_type}:${m.uri}`));
1442
- const merged = [...serverResult];
1443
- for (const m of localResult) {
1444
- const key = `${m.memory_type}:${m.uri}`;
1445
- if (!seen.has(key)) {
1446
- merged.push(m);
1447
- seen.add(key);
1448
- }
1449
- }
1450
- return merged;
1451
1256
  }
1452
1257
  /**
1453
1258
  * 放弃 Memory 更新状态 (v1.21.0 改为走 release API)
@@ -1465,24 +1270,13 @@ export class McpServer {
1465
1270
  }
1466
1271
  // 调 Server release API 释放 claim
1467
1272
  await this.memoryStore.releaseMemoryClaim(uri);
1468
- // 同时清除本地 roleMemoryStore 的 updating 状态(如果是 project_memory)
1469
- // 兄弟实例创建的记忆本地不存在时静默跳过,服务端 release 才是权威操作
1470
1273
  if (record?.memory_type === 'project_memory') {
1471
- try {
1472
- await this.roleMemoryStore.updateMemory({
1473
- uri,
1474
- memory_type: 'project_memory',
1475
- });
1476
- // 不传 memory_status → MemoryStore 默认重置为 normal
1477
- }
1478
- catch (e) {
1479
- logger.info(`[McpServer] abandon_memory_update: 本地无此记忆,跳过本地状态清理: ${uri}`);
1480
- }
1274
+ this.backupProjectMemoryToLocal({ uri, memory_type: 'project_memory' }).catch((e) => {
1275
+ logger.info(`[McpServer] abandon_memory_update: 同步本地备份失败: ${uri}`, e);
1276
+ });
1481
1277
  }
1482
- // 从去重缓存移除
1483
- this.claimedCache.delete(uri);
1484
- // 所有 claim 已释放,停止续期定时器避免无意义请求
1485
- if (this.claimedCache.size === 0) {
1278
+ this.activeLeaseUris.delete(uri);
1279
+ if (this.activeLeaseUris.size === 0) {
1486
1280
  this.stopLeaseRenewal();
1487
1281
  }
1488
1282
  return {
@@ -1571,7 +1365,7 @@ export class McpServer {
1571
1365
  catch (e) {
1572
1366
  logger.warn('[McpServer] 清理回忆中记忆失败: %s', e instanceof Error ? e.message : String(e));
1573
1367
  }
1574
- await this.reportCombinedMemoryStats();
1368
+ await this.reportMemoryStatsToServer();
1575
1369
  const deletedCount = result.deleted ?? 0;
1576
1370
  return {
1577
1371
  deleted: deletedCount,
@@ -1669,26 +1463,6 @@ export class McpServer {
1669
1463
  }
1670
1464
  return false;
1671
1465
  }
1672
- /**
1673
- * 带 null 结果缓存的 Memory 通知构建。
1674
- * 主流程:查缓存 → 命中且未过期则返回 null → 未命中则调 buildFn →
1675
- * 结果为 null 则写入缓存(TTL 60s)→ 结果非 null 则清除缓存并返回。
1676
- * 目的:while(true) 每 30s 轮询时避免重复读盘,有通知时立即投递不受缓存影响。
1677
- */
1678
- async getCachedMemoryNotification(key, buildFn) {
1679
- const expiresAt = this.memoryNotificationNullCache.get(key);
1680
- if (expiresAt !== undefined && expiresAt > Date.now()) {
1681
- return null;
1682
- }
1683
- const result = await buildFn();
1684
- if (result === null) {
1685
- this.memoryNotificationNullCache.set(key, Date.now() + McpServer.MEMORY_NOTIFICATION_NULL_TTL_MS);
1686
- }
1687
- else {
1688
- this.memoryNotificationNullCache.delete(key);
1689
- }
1690
- return result;
1691
- }
1692
1466
  async handlePollMessage() {
1693
1467
  logger.info('[McpServer] handlePollMessage 被调用');
1694
1468
  if (this.pendingPollMessage) {
@@ -1773,8 +1547,8 @@ export class McpServer {
1773
1547
  if (todoNotifLoop) {
1774
1548
  return this.buildPollResult({ directMessages: [todoNotifLoop], groupMessages: [] });
1775
1549
  }
1776
- // 非紧急类:无紧急类时,检查所有非紧急项
1777
- const nonUrgent = await this.checkAllLocalNonUrgentNotifications();
1550
+ // 非紧急类:无紧急类时,检查非紧急项(文件变更仅在入口处检测一次,循环内跳过)
1551
+ const nonUrgent = await this.checkAllLocalNonUrgentNotifications(false);
1778
1552
  if (nonUrgent.length > 0) {
1779
1553
  return this.buildPollResult({ directMessages: nonUrgent, groupMessages: [] });
1780
1554
  }
@@ -1783,19 +1557,19 @@ export class McpServer {
1783
1557
  /**
1784
1558
  * 检查本地非紧急通知(memory过期回忆、文件变更、容量整理),返回所有命中的通知。
1785
1559
  * Task 不在此方法中检查,因为 Task 通过服务端 hydrate 以消息形式返回。
1560
+ * @param includeFileChange 是否包含文件变更检测,默认 true;循环轮询时传 false 以跳过。
1786
1561
  */
1787
- async checkAllLocalNonUrgentNotifications() {
1562
+ async checkAllLocalNonUrgentNotifications(includeFileChange = true) {
1788
1563
  const messages = [];
1789
- // Memory 过期回忆
1790
- const recurrenceNotif = await this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification());
1564
+ const recurrenceNotif = await this.buildMemoryRecurrenceNotification();
1791
1565
  if (recurrenceNotif)
1792
1566
  messages.push(recurrenceNotif);
1793
- // Memory 文件变更(需要更新)
1794
- const fileChangeNotif = await this.buildMemoryFileChangeNotification();
1795
- if (fileChangeNotif)
1796
- messages.push(fileChangeNotif);
1797
- // Memory 容量整理
1798
- const cleanupNotif = await this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification());
1567
+ if (includeFileChange) {
1568
+ const fileChangeNotif = await this.buildMemoryFileChangeNotification();
1569
+ if (fileChangeNotif)
1570
+ messages.push(fileChangeNotif);
1571
+ }
1572
+ const cleanupNotif = await this.buildMemoryCleanupNotification();
1799
1573
  if (cleanupNotif)
1800
1574
  messages.push(cleanupNotif);
1801
1575
  return messages;
@@ -2028,37 +1802,46 @@ export class McpServer {
2028
1802
  },
2029
1803
  };
2030
1804
  }
2031
- // Step1「仍 updating」提醒去重:sticky 的 updating memory 若不在冷却窗口内才发提醒,
2032
- // 否则本轮直接返回 null,避免每轮 poll 都裸发 _acodeAutoDelegate 触发运行层反复 spawn 子任务。
2033
- const now = Date.now();
2034
- const pendingToNotify = updatingMemories.filter(m => {
2035
- const last = this.lastUpdatePendingNotifyAt.get(m.uri) ?? 0;
2036
- return now - last >= McpServer.UPDATE_PENDING_NOTIFY_COOLDOWN_MS;
1805
+ const pendingToNotify = updatingMemories;
1806
+ const pendingStatuses = new Map();
1807
+ for (const m of pendingToNotify.slice(0, 5)) {
1808
+ pendingStatuses.set(m.uri, await this.computeBoundFileStatuses(m));
1809
+ }
1810
+ const memoriesWithRealChanges = pendingToNotify.filter(m => {
1811
+ const statuses = pendingStatuses.get(m.uri);
1812
+ if (!statuses || statuses.length === 0)
1813
+ return false;
1814
+ return statuses.some(s => s.status === 'content_changed' || s.status === 'not_found');
2037
1815
  });
2038
- // 清理已不在 updating 列表中的过期冷却记录
2039
- for (const uri of this.lastUpdatePendingNotifyAt.keys()) {
2040
- if (!updatingMemories.some(m => m.uri === uri)) {
2041
- this.lastUpdatePendingNotifyAt.delete(uri);
1816
+ const staleUpdatingMemories = pendingToNotify.filter(m => !memoriesWithRealChanges.includes(m));
1817
+ if (staleUpdatingMemories.length > 0) {
1818
+ logger.info(`[McpServer] 文件变更检测 Step1: ${staleUpdatingMemories.length} 个 updating memory 绑定文件无真实变更,自动释放: ${staleUpdatingMemories.map(m => m.uri).join(', ')}`);
1819
+ for (const m of staleUpdatingMemories) {
1820
+ try {
1821
+ await this.memoryStore.releaseMemoryClaim(m.uri);
1822
+ this.activeLeaseUris.delete(m.uri);
1823
+ await this.memoryStore.updateMemory({
1824
+ uri: m.uri,
1825
+ memory_type: 'project_memory',
1826
+ });
1827
+ }
1828
+ catch (e) {
1829
+ logger.info(`[McpServer] 自动释放 stale updating memory 失败: ${m.uri}`, e);
1830
+ }
1831
+ }
1832
+ if (this.activeLeaseUris.size === 0) {
1833
+ this.stopLeaseRenewal();
2042
1834
  }
2043
1835
  }
2044
- if (pendingToNotify.length === 0) {
2045
- logger.info(`[McpServer] 文件变更检测 Step1: ${updatingMemories.length} updating memory 均处于冷却窗口内,跳过提醒`);
1836
+ if (memoriesWithRealChanges.length === 0) {
1837
+ logger.info(`[McpServer] 文件变更检测 Step1: 所有 updating memory 绑定文件均无真实变更,已自动释放,跳过委托`);
2046
1838
  return null;
2047
1839
  }
2048
- for (const m of pendingToNotify) {
2049
- this.lastUpdatePendingNotifyAt.set(m.uri, now);
2050
- }
2051
- // 计算 pending(仍 updating)记忆的逐文件状态,供子 agent 在提示词中标注
2052
- // (无变更/内容变更/文件找不到),避免「本次变更文件: 未知」歧义。
2053
- const pendingStatuses = new Map();
2054
- for (const m of pendingToNotify.slice(0, 5)) {
2055
- pendingStatuses.set(m.uri, await this.computeBoundFileStatuses(m));
2056
- }
2057
- const memoryList = pendingToNotify
1840
+ const memoryList = memoriesWithRealChanges
2058
1841
  .slice(0, 5)
2059
1842
  .map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
2060
1843
  .join('\n');
2061
- logger.info(`[McpServer] 文件变更检测 Step1: ${pendingToNotify.length}/${updatingMemories.length} 个 updating memory 待更新(其余冷却中),租约已续期`);
1844
+ logger.info(`[McpServer] 文件变更检测 Step1: ${memoriesWithRealChanges.length}/${updatingMemories.length} 个 updating memory 有真实文件变更,触发委托`);
2062
1845
  return {
2063
1846
  msgId: `memory-update-pending-${Date.now()}`,
2064
1847
  senderId: "MCP-System",
@@ -2070,10 +1853,8 @@ export class McpServer {
2070
1853
  timestamp: new Date().toISOString(),
2071
1854
  _acodeAutoDelegate: {
2072
1855
  type: 'memory_file_change',
2073
- uris: pendingToNotify.slice(0, 5).map(m => m.uri),
2074
- // 此分支为「仍 updating」状态提醒,本轮未检测到新的文件变更,
2075
- // 仅传递绑定文件清单与逐文件状态供子 agent 直接定位文件,changedFiles 留空。
2076
- fileChanges: pendingToNotify.slice(0, 5).map(m => ({
1856
+ uris: memoriesWithRealChanges.slice(0, 5).map(m => m.uri),
1857
+ fileChanges: memoriesWithRealChanges.slice(0, 5).map(m => ({
2077
1858
  uri: m.uri,
2078
1859
  boundFiles: m.bound_files ?? [],
2079
1860
  fileStatuses: pendingStatuses.get(m.uri) ?? [],
@@ -2090,15 +1871,6 @@ export class McpServer {
2090
1871
  logger.info(`[McpServer] 文件变更检测 Step2: recently=${recentlyMemoryMemories.length}, agent=${agentMemoryMemories.length}, ` +
2091
1872
  `有绑定文件=${listableWithFiles.length}/${listableMemories.length}。` +
2092
1873
  `详情: ${listableMemories.map(m => `${m.uri}(bound=${m.bound_files?.length ?? 0},md5=${Object.keys(m.file_md5_map ?? {}).length})`).join('; ') || '无 memory'}`);
2093
- // 用本地 MD5 缓存补全 listable 记录(服务端可能不持久化 file_md5_map 字段)
2094
- for (const m of listableMemories) {
2095
- if (!m.file_md5_map) {
2096
- const cached = this.listableMd5Cache.get(m.id);
2097
- if (cached) {
2098
- m.file_md5_map = cached;
2099
- }
2100
- }
2101
- }
2102
1874
  // 捕获检测前的 md5 map 快照,用于检测 checkMemoriesFileChanges 是否修复了键缺失
2103
1875
  // (路径规范化不一致导致 bound_files 与 file_md5_map 键不匹配时的静默修正)
2104
1876
  const listableMd5Snapshots = new Map();
@@ -2108,18 +1880,8 @@ export class McpServer {
2108
1880
  }
2109
1881
  }
2110
1882
  const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
2111
- const newListableChanges = listableChanges.filter(c => {
2112
- const memory = listableMemories.find(m => m.id === c.memoryId);
2113
- const md5Map = memory?.file_md5_map;
2114
- const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
2115
- const lastNotified = this.notifiedFileChangeMemoryIds.get(c.memoryId);
2116
- const isNew = lastNotified !== changeKey;
2117
- if (!isNew) {
2118
- logger.info(`[McpServer] 文件变更检测 Step2: ${c.memoryUri} 变更被去重吞掉 (changeKey=${changeKey}, lastNotified=${lastNotified})`);
2119
- }
2120
- return isNew;
2121
- });
2122
- logger.info(`[McpServer] 文件变更检测 Step2: 检测到变更=${listableChanges.length}, 去重后=${newListableChanges.length}` +
1883
+ const newListableChanges = listableChanges;
1884
+ logger.info(`[McpServer] 文件变更检测 Step2: 检测到变更=${listableChanges.length}` +
2123
1885
  (listableChanges.length > 0 ? ` 变更项: ${listableChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
2124
1886
  // 回写被修复的 md5 map(键缺失修正)到服务端,避免后续轮次因键不匹配反复误判。
2125
1887
  // 仅回写未发生真实变更的 memory(发生变更的已在下方 claim/通知流程中处理)。
@@ -2142,9 +1904,6 @@ export class McpServer {
2142
1904
  file_md5_map: m.file_md5_map,
2143
1905
  last_check_time: m.last_check_time,
2144
1906
  });
2145
- if (m.file_md5_map) {
2146
- this.listableMd5Cache.set(m.id, m.file_md5_map);
2147
- }
2148
1907
  }
2149
1908
  catch (e) {
2150
1909
  logger.info(`[McpServer] 回写 listable MD5 map 失败: ${m.uri}`, e);
@@ -2152,14 +1911,6 @@ export class McpServer {
2152
1911
  }
2153
1912
  }
2154
1913
  if (newListableChanges.length > 0) {
2155
- for (const c of newListableChanges) {
2156
- const memory = listableMemories.find(m => m.id === c.memoryId);
2157
- const md5Map = memory?.file_md5_map;
2158
- this.notifiedFileChangeMemoryIds.set(c.memoryId, this.buildMemoryChangeNotificationKey(c.memoryId, md5Map));
2159
- if (memory?.file_md5_map) {
2160
- this.listableMd5Cache.set(c.memoryId, memory.file_md5_map);
2161
- }
2162
- }
2163
1914
  const changeDetails = newListableChanges
2164
1915
  .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
2165
1916
  .join('\n');
@@ -2264,8 +2015,7 @@ export class McpServer {
2264
2015
  last_check_time: memory.last_check_time,
2265
2016
  bound_files: memory.bound_files,
2266
2017
  });
2267
- // 加入去重缓存(lease 有效期内不再重复 claim)
2268
- this.claimedCache.set(memory.uri, Date.now() + McpServer.LEASE_DURATION_MINUTES * 60 * 1000);
2018
+ this.activeLeaseUris.add(memory.uri);
2269
2019
  }
2270
2020
  }
2271
2021
  // 启动租约续期(仅 Server claim 成功时需要,降级模式不续期)
@@ -2308,19 +2058,8 @@ export class McpServer {
2308
2058
  return null;
2309
2059
  }
2310
2060
  }
2311
- /**
2312
- * 检查指定 URI 是否在 claim 去重缓存内 (v1.21.0)。
2313
- * 在 lease 有效期内不再重复 claim。
2314
- */
2315
2061
  isClaimCached(uri) {
2316
- const expiry = this.claimedCache.get(uri);
2317
- if (expiry === undefined)
2318
- return false;
2319
- if (expiry <= Date.now()) {
2320
- this.claimedCache.delete(uri);
2321
- return false;
2322
- }
2323
- return true;
2062
+ return this.activeLeaseUris.has(uri);
2324
2063
  }
2325
2064
  /**
2326
2065
  * 计算某条记忆全部绑定文件的逐文件状态(纯计算,不修改任何状态)。
@@ -2334,11 +2073,19 @@ export class McpServer {
2334
2073
  if (!memory.bound_files || memory.bound_files.length === 0)
2335
2074
  return result;
2336
2075
  const oldMd5Map = memory.file_md5_map || {};
2076
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
2337
2077
  for (const filePath of memory.bound_files) {
2338
2078
  try {
2339
2079
  const currentMd5 = await this.calculateFileMd5(filePath);
2340
2080
  if (currentMd5 === null) {
2341
- result.push({ path: filePath, status: 'not_found' });
2081
+ const resolvedPath = path.resolve(workspacePath, filePath);
2082
+ const fileTrulyMissing = await this.verifyFileNotExist(resolvedPath);
2083
+ if (fileTrulyMissing && oldMd5Map[filePath]) {
2084
+ result.push({ path: filePath, status: 'not_found', serverMd5: oldMd5Map[filePath] });
2085
+ }
2086
+ else {
2087
+ result.push({ path: filePath, status: 'no_change' });
2088
+ }
2342
2089
  continue;
2343
2090
  }
2344
2091
  const oldMd5 = oldMd5Map[filePath];
@@ -2346,14 +2093,13 @@ export class McpServer {
2346
2093
  result.push({ path: filePath, status: 'no_change' });
2347
2094
  }
2348
2095
  else if (oldMd5 !== currentMd5) {
2349
- result.push({ path: filePath, status: 'content_changed' });
2096
+ result.push({ path: filePath, status: 'content_changed', localMd5: currentMd5, serverMd5: oldMd5 });
2350
2097
  }
2351
2098
  else {
2352
2099
  result.push({ path: filePath, status: 'no_change' });
2353
2100
  }
2354
2101
  }
2355
2102
  catch {
2356
- // MD5 计算失败:保守视为未变更,不误报
2357
2103
  result.push({ path: filePath, status: 'no_change' });
2358
2104
  }
2359
2105
  }
@@ -2382,43 +2128,39 @@ export class McpServer {
2382
2128
  try {
2383
2129
  const currentMd5 = await this.calculateFileMd5(filePath);
2384
2130
  if (currentMd5 === null) {
2385
- // 文件不存在或无法读取
2386
2131
  const resolvedPath = path.resolve(workspacePath, filePath);
2387
2132
  logger.info(`[McpServer] MD5 计算返回 null: ${filePath} -> ${resolvedPath} (workspace=${workspacePath})`);
2388
2133
  const oldMd5ForMissing = oldMd5Map[filePath];
2389
2134
  if (oldMd5ForMissing) {
2390
- // 该文件曾有基线,现在读不到 -> 视为变更(文件被删除/移动)
2391
- statuses.push({ path: filePath, status: 'not_found' });
2392
- changedFiles.push(`${filePath} (文件找不到)`);
2393
- // 收口:文件已消失,从 bound_files 移除该路径,消除悬空引用。
2394
- // 否则 listByFile/findByBoundFile 会持续命中无效记录,且文件恢复后因基线已清而变更检测失效。
2395
- // bound_files 修剪结果由调用方在变更回写分支同步持久化(见 buildMemoryFileChangeNotification)。
2396
- if (memory.bound_files) {
2397
- memory.bound_files = memory.bound_files.filter(f => f !== filePath);
2135
+ const fileTrulyMissing = await this.verifyFileNotExist(resolvedPath);
2136
+ if (fileTrulyMissing) {
2137
+ statuses.push({ path: filePath, status: 'not_found', serverMd5: oldMd5ForMissing });
2138
+ changedFiles.push(`${filePath}(文件找不到 Server: ${oldMd5ForMissing})`);
2139
+ if (memory.bound_files) {
2140
+ memory.bound_files = memory.bound_files.filter(f => f !== filePath);
2141
+ }
2142
+ }
2143
+ else {
2144
+ logger.info(`[McpServer] 文件存在但 MD5 计算失败(瞬态错误),跳过: ${filePath}`);
2145
+ statuses.push({ path: filePath, status: 'no_change' });
2398
2146
  }
2399
2147
  }
2400
2148
  else {
2401
- // 无该文件基线且文件不存在:标注找不到但不计入变更,避免对未知状态误报
2402
- statuses.push({ path: filePath, status: 'not_found' });
2149
+ statuses.push({ path: filePath, status: 'no_change' });
2403
2150
  }
2404
- // 无该文件基线时静默跳过,避免对未知状态误报
2405
2151
  continue;
2406
2152
  }
2407
2153
  newMd5Map[filePath] = currentMd5;
2408
2154
  const oldMd5 = oldMd5Map[filePath];
2409
2155
  if (!oldMd5) {
2410
- // 该文件无可用基线(键缺失或空值)。常见原因:
2411
- // 1) 新增到 bound_files 的文件尚未建基线;
2412
- // 2) 历史 TS/Java 路径规范化不一致(如 Windows 反斜杠)导致键不匹配。
2413
- // 静默建立基线,不视为变更,避免 md5 实际未变却误报需要更新。
2414
2156
  statuses.push({ path: filePath, status: 'no_change' });
2415
2157
  baselineRepaired = true;
2416
2158
  logger.info(`[McpServer] 建立单文件基线: ${filePath} new=${currentMd5} (workspace=${workspacePath}, memory=${memory.uri})`);
2417
2159
  }
2418
2160
  else if (oldMd5 !== currentMd5) {
2419
2161
  logger.info(`[McpServer] 文件变更: ${filePath} old=${oldMd5} new=${currentMd5} (workspace=${workspacePath})`);
2420
- statuses.push({ path: filePath, status: 'content_changed' });
2421
- changedFiles.push(`${filePath} (内容变更)`);
2162
+ statuses.push({ path: filePath, status: 'content_changed', localMd5: currentMd5, serverMd5: oldMd5 });
2163
+ changedFiles.push(`${filePath}(内容变更 本地: ${currentMd5} Server: ${oldMd5})`);
2422
2164
  }
2423
2165
  else {
2424
2166
  statuses.push({ path: filePath, status: 'no_change' });
@@ -2426,14 +2168,7 @@ export class McpServer {
2426
2168
  }
2427
2169
  catch (error) {
2428
2170
  logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
2429
- const oldMd5ForError = oldMd5Map[filePath];
2430
- if (oldMd5ForError) {
2431
- statuses.push({ path: filePath, status: 'content_changed' });
2432
- changedFiles.push(`${filePath} (MD5计算失败)`);
2433
- }
2434
- else {
2435
- statuses.push({ path: filePath, status: 'no_change' });
2436
- }
2171
+ statuses.push({ path: filePath, status: 'no_change' });
2437
2172
  }
2438
2173
  }
2439
2174
  // 无基线时:更新 MD5 map 到 memory 对象(供调用方回写),但不计入变更
@@ -2502,17 +2237,14 @@ export class McpServer {
2502
2237
  return null;
2503
2238
  }
2504
2239
  }
2505
- /**
2506
- * 构建 Memory 文件变更通知的复合去重键。
2507
- * 基于 memoryId + 当前各绑定文件的 MD5 生成,当任意绑定文件内容变化时复合键也随之变化,
2508
- * 从而确保真实文件变更不会因简单 memoryId 去重而静默吞掉。
2509
- */
2510
- buildMemoryChangeNotificationKey(memoryId, md5Map) {
2511
- if (!md5Map || Object.keys(md5Map).length === 0) {
2512
- return memoryId;
2240
+ async verifyFileNotExist(resolvedPath) {
2241
+ try {
2242
+ await fs.promises.access(resolvedPath, fs.constants.F_OK);
2243
+ return false;
2244
+ }
2245
+ catch {
2246
+ return true;
2513
2247
  }
2514
- const sorted = Object.entries(md5Map).sort(([a], [b]) => a.localeCompare(b));
2515
- return `${memoryId}:${sorted.map(([k, v]) => `${k}=${v}`).join('|')}`;
2516
2248
  }
2517
2249
  async handleGetMessage() {
2518
2250
  // 消息来源标记:
@@ -2795,13 +2527,6 @@ export class McpServer {
2795
2527
  logger.warn("[McpServer] 租约已丢失(renewed=0),停止续期定时器");
2796
2528
  this.stopLeaseRenewal();
2797
2529
  }
2798
- // L-1: 主动清理 claimedCache 中的过期条目,避免堆积
2799
- const now = Date.now();
2800
- for (const [uri, expiry] of this.claimedCache) {
2801
- if (expiry <= now) {
2802
- this.claimedCache.delete(uri);
2803
- }
2804
- }
2805
2530
  }
2806
2531
  catch (e) {
2807
2532
  logger.warn('[McpServer] 续期租约失败: %s', e instanceof Error ? e.message : String(e));
@@ -2857,7 +2582,7 @@ export class McpServer {
2857
2582
  if (statusReporter) {
2858
2583
  statusReporter.reportWaitingInput();
2859
2584
  }
2860
- await this.reportCombinedMemoryStats();
2585
+ await this.reportMemoryStatsToServer();
2861
2586
  // 注册成功后检查是否需要从本地恢复永久记忆到 server
2862
2587
  // 触发条件:server 当前角色 memory 计数为 0(重部署后 DB 为空)
2863
2588
  // 不依赖 isExisting / profileHandler,确保所有场景都能触发
@@ -2883,7 +2608,7 @@ export class McpServer {
2883
2608
  if (statusReporter) {
2884
2609
  statusReporter.reportWaitingInput();
2885
2610
  }
2886
- await this.reportCombinedMemoryStats();
2611
+ await this.reportMemoryStatsToServer();
2887
2612
  return;
2888
2613
  }
2889
2614
  // 未启用降级模式,抛出异常