aws-runtime-bridge 1.9.33 → 1.9.41

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.
Files changed (324) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +66 -5
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +771 -69
  4. package/dist/adapter/ClaudeSdkAdapter.d.ts +6 -0
  5. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/ClaudeSdkAdapter.js +24 -1
  7. package/dist/adapter/OpencodeSdkAdapter.d.ts +19 -0
  8. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  9. package/dist/adapter/OpencodeSdkAdapter.js +62 -0
  10. package/dist/adapter/idle-poll-loop.d.ts +2 -1
  11. package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
  12. package/dist/adapter/types.d.ts +86 -9
  13. package/dist/adapter/types.d.ts.map +1 -1
  14. package/dist/adapter/types.js +15 -5
  15. package/dist/index.js +15 -0
  16. package/dist/routes/acode-todos.d.ts +11 -0
  17. package/dist/routes/acode-todos.d.ts.map +1 -0
  18. package/dist/routes/acode-todos.js +75 -0
  19. package/dist/routes/ai-sources.js +4 -1
  20. package/dist/routes/dashboard.d.ts.map +1 -1
  21. package/dist/routes/dashboard.js +1 -1
  22. package/dist/routes/memory.d.ts +1 -1
  23. package/dist/routes/memory.d.ts.map +1 -1
  24. package/dist/routes/memory.js +3 -3
  25. package/dist/routes/pty.d.ts.map +1 -1
  26. package/dist/routes/pty.js +9 -0
  27. package/dist/routes/runtime-binding.d.ts.map +1 -1
  28. package/dist/routes/runtime-binding.js +6 -3
  29. package/dist/routes/sessions.js +89 -4
  30. package/dist/routes/sub-agents.d.ts +14 -0
  31. package/dist/routes/sub-agents.d.ts.map +1 -0
  32. package/dist/routes/sub-agents.js +267 -0
  33. package/dist/routes/terminal.d.ts +12 -1
  34. package/dist/routes/terminal.d.ts.map +1 -1
  35. package/dist/routes/terminal.js +326 -24
  36. package/dist/services/acode-package.d.ts +8 -1
  37. package/dist/services/acode-package.d.ts.map +1 -1
  38. package/dist/services/acode-package.js +33 -4
  39. package/dist/services/agent-event-store.d.ts +1 -0
  40. package/dist/services/agent-event-store.d.ts.map +1 -1
  41. package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
  42. package/dist/services/aws-client-agent-mcp.js +11 -4
  43. package/dist/services/event-delta-pusher.js +4 -4
  44. package/dist/services/fileSnapshotStore.js +1 -1
  45. package/dist/services/legacy-path-migration.d.ts +3 -0
  46. package/dist/services/legacy-path-migration.d.ts.map +1 -0
  47. package/dist/services/legacy-path-migration.js +66 -0
  48. package/dist/services/lifecycle-state.d.ts.map +1 -1
  49. package/dist/services/lifecycle-state.js +1 -1
  50. package/dist/services/runtime-binding.d.ts +5 -0
  51. package/dist/services/runtime-binding.d.ts.map +1 -1
  52. package/dist/services/runtime-binding.js +33 -1
  53. package/dist/services/session-output.d.ts +10 -1
  54. package/dist/services/session-output.d.ts.map +1 -1
  55. package/dist/services/session-output.js +54 -4
  56. package/dist/services/sub-agent-manager-instance.d.ts +4 -0
  57. package/dist/services/sub-agent-manager-instance.d.ts.map +1 -0
  58. package/dist/services/sub-agent-manager-instance.js +12 -0
  59. package/dist/services/workspace-files.d.ts +1 -0
  60. package/dist/services/workspace-files.d.ts.map +1 -1
  61. package/dist/services/workspace-files.js +57 -1
  62. package/package/acode/dist/ask-user-question-tool.d.ts +47 -0
  63. package/package/acode/dist/ask-user-question-tool.d.ts.map +1 -0
  64. package/package/acode/dist/ask-user-question-tool.js +233 -0
  65. package/package/acode/dist/built-in-file-tools.d.ts +2 -0
  66. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  67. package/package/acode/dist/built-in-file-tools.js +504 -3
  68. package/package/acode/dist/compaction.d.ts +242 -0
  69. package/package/acode/dist/compaction.d.ts.map +1 -0
  70. package/package/acode/dist/compaction.js +919 -0
  71. package/package/acode/dist/config.d.ts +6 -1
  72. package/package/acode/dist/config.d.ts.map +1 -1
  73. package/package/acode/dist/config.js +27 -2
  74. package/package/acode/dist/index.d.ts +4 -0
  75. package/package/acode/dist/index.d.ts.map +1 -1
  76. package/package/acode/dist/index.js +4 -0
  77. package/package/acode/dist/runtime.d.ts +257 -0
  78. package/package/acode/dist/runtime.d.ts.map +1 -1
  79. package/package/acode/dist/runtime.js +1843 -191
  80. package/package/acode/dist/skill-tool.d.ts +30 -0
  81. package/package/acode/dist/skill-tool.d.ts.map +1 -0
  82. package/package/acode/dist/skill-tool.js +174 -0
  83. package/package/acode/dist/sub-agent-manager.d.ts +264 -0
  84. package/package/acode/dist/sub-agent-manager.d.ts.map +1 -0
  85. package/package/acode/dist/sub-agent-manager.js +825 -0
  86. package/package/acode/dist/sub-agent-store.d.ts +40 -0
  87. package/package/acode/dist/sub-agent-store.d.ts.map +1 -0
  88. package/package/acode/dist/sub-agent-store.js +164 -0
  89. package/package/acode/dist/sub-agent-tools.d.ts +72 -0
  90. package/package/acode/dist/sub-agent-tools.d.ts.map +1 -0
  91. package/package/acode/dist/sub-agent-tools.js +429 -0
  92. package/package/acode/dist/sub-agent-types.d.ts +85 -0
  93. package/package/acode/dist/sub-agent-types.d.ts.map +1 -0
  94. package/package/acode/dist/sub-agent-types.js +8 -0
  95. package/package/acode/dist/todo-store.d.ts +42 -0
  96. package/package/acode/dist/todo-store.d.ts.map +1 -0
  97. package/package/acode/dist/todo-store.js +68 -0
  98. package/package/acode/dist/todo-tool.d.ts +6 -0
  99. package/package/acode/dist/todo-tool.d.ts.map +1 -0
  100. package/package/acode/dist/todo-tool.js +138 -0
  101. package/package/acode/dist/types.d.ts +41 -1
  102. package/package/acode/dist/types.d.ts.map +1 -1
  103. package/package/acode/package.json +13 -2
  104. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +9 -0
  105. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/agent-client.js +69 -10
  107. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
  109. package/package/aws-client-agent-mcp/dist/config.js +4 -0
  110. package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
  111. package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
  112. package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
  113. package/package/aws-client-agent-mcp/dist/constants.js +3 -1
  114. package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
  115. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +4 -1
  116. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/context-manager.js +6 -2
  118. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/context-manager.test.js +5 -3
  120. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
  121. package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
  122. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  123. package/package/aws-client-agent-mcp/dist/http-client.js +15 -1
  124. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  125. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +162 -71
  126. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  127. package/package/aws-client-agent-mcp/dist/mcp-server.js +1007 -504
  128. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  129. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +371 -253
  130. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  131. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +6 -17
  132. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  133. package/package/aws-client-agent-mcp/dist/mcp-tools.js +20 -75
  134. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  135. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +45 -45
  136. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  137. package/package/aws-client-agent-mcp/dist/memory-store.js +114 -162
  138. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  139. package/package/aws-client-agent-mcp/dist/memory-store.test.js +175 -164
  140. package/package/aws-client-agent-mcp/dist/memory-store.test.js.map +1 -1
  141. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +3 -9
  142. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  143. package/package/aws-client-agent-mcp/dist/memory-tools.js +31 -58
  144. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  145. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +40 -9
  146. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
  147. package/package/aws-client-agent-mcp/dist/message-buffer.js +123 -23
  148. package/package/aws-client-agent-mcp/dist/message-buffer.js.map +1 -1
  149. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +13 -13
  150. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  151. package/package/aws-client-agent-mcp/dist/server-memory-store.js +23 -16
  152. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  153. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
  154. package/package/aws-client-agent-mcp/dist/status-reporter.js +8 -5
  155. package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
  156. package/package/aws-client-agent-mcp/dist/status-reporter.test.js +93 -0
  157. package/package/aws-client-agent-mcp/dist/status-reporter.test.js.map +1 -1
  158. package/package/aws-client-agent-mcp/dist/types.d.ts +12 -25
  159. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  160. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  161. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +34 -1
  162. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  163. package/package/aws-client-agent-mcp/dist/websocket-client.js +146 -18
  164. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  165. package/package.json +17 -7
  166. package/dist/adapter/AcodeSdkAdapter.test.d.ts +0 -2
  167. package/dist/adapter/AcodeSdkAdapter.test.d.ts.map +0 -1
  168. package/dist/adapter/AcodeSdkAdapter.test.js +0 -2000
  169. package/dist/adapter/AdapterRegistry.test.d.ts +0 -5
  170. package/dist/adapter/AdapterRegistry.test.d.ts.map +0 -1
  171. package/dist/adapter/AdapterRegistry.test.js +0 -118
  172. package/dist/adapter/ClaudeSdkAdapter.test.d.ts +0 -2
  173. package/dist/adapter/ClaudeSdkAdapter.test.d.ts.map +0 -1
  174. package/dist/adapter/ClaudeSdkAdapter.test.js +0 -311
  175. package/dist/adapter/CodexSdkAdapter.test.d.ts +0 -2
  176. package/dist/adapter/CodexSdkAdapter.test.d.ts.map +0 -1
  177. package/dist/adapter/CodexSdkAdapter.test.js +0 -270
  178. package/dist/adapter/OpencodeSdkAdapter.test.d.ts +0 -2
  179. package/dist/adapter/OpencodeSdkAdapter.test.d.ts.map +0 -1
  180. package/dist/adapter/OpencodeSdkAdapter.test.js +0 -158
  181. package/dist/adapter/acode-tool-names.test.d.ts +0 -2
  182. package/dist/adapter/acode-tool-names.test.d.ts.map +0 -1
  183. package/dist/adapter/acode-tool-names.test.js +0 -28
  184. package/dist/adapter/adapter.test.d.ts +0 -5
  185. package/dist/adapter/adapter.test.d.ts.map +0 -1
  186. package/dist/adapter/adapter.test.js +0 -180
  187. package/dist/adapter/idle-poll-loop.test.d.ts +0 -2
  188. package/dist/adapter/idle-poll-loop.test.d.ts.map +0 -1
  189. package/dist/adapter/idle-poll-loop.test.js +0 -85
  190. package/dist/adapter/types.test.d.ts +0 -2
  191. package/dist/adapter/types.test.d.ts.map +0 -1
  192. package/dist/adapter/types.test.js +0 -50
  193. package/dist/adapters/cc-switch/mcp-acode.test.d.ts +0 -2
  194. package/dist/adapters/cc-switch/mcp-acode.test.d.ts.map +0 -1
  195. package/dist/adapters/cc-switch/mcp-acode.test.js +0 -80
  196. package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts +0 -2
  197. package/dist/adapters/cc-switch/mcp-claudecode.test.d.ts.map +0 -1
  198. package/dist/adapters/cc-switch/mcp-claudecode.test.js +0 -75
  199. package/dist/adapters/cc-switch/mcp-codex.test.d.ts +0 -2
  200. package/dist/adapters/cc-switch/mcp-codex.test.d.ts.map +0 -1
  201. package/dist/adapters/cc-switch/mcp-codex.test.js +0 -126
  202. package/dist/adapters/cc-switch/mcp-opencode.test.d.ts +0 -2
  203. package/dist/adapters/cc-switch/mcp-opencode.test.d.ts.map +0 -1
  204. package/dist/adapters/cc-switch/mcp-opencode.test.js +0 -62
  205. package/dist/adapters/cc-switch/skill-directory.test.d.ts +0 -2
  206. package/dist/adapters/cc-switch/skill-directory.test.d.ts.map +0 -1
  207. package/dist/adapters/cc-switch/skill-directory.test.js +0 -74
  208. package/dist/config.test.d.ts +0 -2
  209. package/dist/config.test.d.ts.map +0 -1
  210. package/dist/config.test.js +0 -107
  211. package/dist/routes/ai-sources.test.d.ts +0 -2
  212. package/dist/routes/ai-sources.test.d.ts.map +0 -1
  213. package/dist/routes/ai-sources.test.js +0 -169
  214. package/dist/routes/file-browser.test.d.ts +0 -5
  215. package/dist/routes/file-browser.test.d.ts.map +0 -1
  216. package/dist/routes/file-browser.test.js +0 -155
  217. package/dist/routes/git.test.d.ts +0 -2
  218. package/dist/routes/git.test.d.ts.map +0 -1
  219. package/dist/routes/git.test.js +0 -330
  220. package/dist/routes/instance.test.d.ts +0 -5
  221. package/dist/routes/instance.test.d.ts.map +0 -1
  222. package/dist/routes/instance.test.js +0 -389
  223. package/dist/routes/mcp.test.d.ts +0 -5
  224. package/dist/routes/mcp.test.d.ts.map +0 -1
  225. package/dist/routes/mcp.test.js +0 -92
  226. package/dist/routes/memory-route-wiring.test.d.ts +0 -2
  227. package/dist/routes/memory-route-wiring.test.d.ts.map +0 -1
  228. package/dist/routes/memory-route-wiring.test.js +0 -15
  229. package/dist/routes/memory.test.d.ts +0 -2
  230. package/dist/routes/memory.test.d.ts.map +0 -1
  231. package/dist/routes/memory.test.js +0 -177
  232. package/dist/routes/properties.test.d.ts +0 -5
  233. package/dist/routes/properties.test.d.ts.map +0 -1
  234. package/dist/routes/properties.test.js +0 -72
  235. package/dist/routes/pty.test.d.ts +0 -2
  236. package/dist/routes/pty.test.d.ts.map +0 -1
  237. package/dist/routes/pty.test.js +0 -82
  238. package/dist/routes/runtime-binding.test.d.ts +0 -2
  239. package/dist/routes/runtime-binding.test.d.ts.map +0 -1
  240. package/dist/routes/runtime-binding.test.js +0 -32
  241. package/dist/routes/sessions.test.d.ts +0 -5
  242. package/dist/routes/sessions.test.d.ts.map +0 -1
  243. package/dist/routes/sessions.test.js +0 -86
  244. package/dist/routes/skills.test.d.ts +0 -5
  245. package/dist/routes/skills.test.d.ts.map +0 -1
  246. package/dist/routes/skills.test.js +0 -50
  247. package/dist/routes/system-metrics.test.d.ts +0 -2
  248. package/dist/routes/system-metrics.test.d.ts.map +0 -1
  249. package/dist/routes/system-metrics.test.js +0 -16
  250. package/dist/routes/terminal.stop-options.test.d.ts +0 -2
  251. package/dist/routes/terminal.stop-options.test.d.ts.map +0 -1
  252. package/dist/routes/terminal.stop-options.test.js +0 -39
  253. package/dist/routes/terminal.test.d.ts +0 -2
  254. package/dist/routes/terminal.test.d.ts.map +0 -1
  255. package/dist/routes/terminal.test.js +0 -746
  256. package/dist/routes/yml.test.d.ts +0 -5
  257. package/dist/routes/yml.test.d.ts.map +0 -1
  258. package/dist/routes/yml.test.js +0 -58
  259. package/dist/services/ai-source-apply.test.d.ts +0 -2
  260. package/dist/services/ai-source-apply.test.d.ts.map +0 -1
  261. package/dist/services/ai-source-apply.test.js +0 -138
  262. package/dist/services/auto-register.test.d.ts +0 -2
  263. package/dist/services/auto-register.test.d.ts.map +0 -1
  264. package/dist/services/auto-register.test.js +0 -184
  265. package/dist/services/aws-client-agent-mcp.test.d.ts +0 -2
  266. package/dist/services/aws-client-agent-mcp.test.d.ts.map +0 -1
  267. package/dist/services/aws-client-agent-mcp.test.js +0 -242
  268. package/dist/services/bridge-package-publish-hooks.test.d.ts +0 -2
  269. package/dist/services/bridge-package-publish-hooks.test.d.ts.map +0 -1
  270. package/dist/services/bridge-package-publish-hooks.test.js +0 -136
  271. package/dist/services/cc-switch-discovery.test.d.ts +0 -2
  272. package/dist/services/cc-switch-discovery.test.d.ts.map +0 -1
  273. package/dist/services/cc-switch-discovery.test.js +0 -56
  274. package/dist/services/cli-commands.test.d.ts +0 -2
  275. package/dist/services/cli-commands.test.d.ts.map +0 -1
  276. package/dist/services/cli-commands.test.js +0 -230
  277. package/dist/services/instance-init-service.test.d.ts +0 -2
  278. package/dist/services/instance-init-service.test.d.ts.map +0 -1
  279. package/dist/services/instance-init-service.test.js +0 -140
  280. package/dist/services/instance-state.test.d.ts +0 -2
  281. package/dist/services/instance-state.test.d.ts.map +0 -1
  282. package/dist/services/instance-state.test.js +0 -245
  283. package/dist/services/mcp-launch-binding-queue.test.d.ts +0 -2
  284. package/dist/services/mcp-launch-binding-queue.test.d.ts.map +0 -1
  285. package/dist/services/mcp-launch-binding-queue.test.js +0 -181
  286. package/dist/services/runtime-binding.test.d.ts +0 -2
  287. package/dist/services/runtime-binding.test.d.ts.map +0 -1
  288. package/dist/services/runtime-binding.test.js +0 -78
  289. package/dist/services/runtime-lifecycle-policy.test.d.ts +0 -2
  290. package/dist/services/runtime-lifecycle-policy.test.d.ts.map +0 -1
  291. package/dist/services/runtime-lifecycle-policy.test.js +0 -41
  292. package/dist/services/session-output.test.d.ts +0 -5
  293. package/dist/services/session-output.test.d.ts.map +0 -1
  294. package/dist/services/session-output.test.js +0 -177
  295. package/dist/services/startup-config-wizard.test.d.ts +0 -2
  296. package/dist/services/startup-config-wizard.test.d.ts.map +0 -1
  297. package/dist/services/startup-config-wizard.test.js +0 -184
  298. package/dist/services/terminal-persistence.test.d.ts +0 -5
  299. package/dist/services/terminal-persistence.test.d.ts.map +0 -1
  300. package/dist/services/terminal-persistence.test.js +0 -159
  301. package/dist/services/tool-installer.test.d.ts +0 -2
  302. package/dist/services/tool-installer.test.d.ts.map +0 -1
  303. package/dist/services/tool-installer.test.js +0 -225
  304. package/dist/services/workspace-files.test.d.ts +0 -2
  305. package/dist/services/workspace-files.test.d.ts.map +0 -1
  306. package/dist/services/workspace-files.test.js +0 -648
  307. package/dist/services/workspace-watch.test.d.ts +0 -2
  308. package/dist/services/workspace-watch.test.d.ts.map +0 -1
  309. package/dist/services/workspace-watch.test.js +0 -38
  310. package/dist/utils/file-utils.test.d.ts +0 -2
  311. package/dist/utils/file-utils.test.d.ts.map +0 -1
  312. package/dist/utils/file-utils.test.js +0 -244
  313. package/dist/utils/logger.test.d.ts +0 -2
  314. package/dist/utils/logger.test.d.ts.map +0 -1
  315. package/dist/utils/logger.test.js +0 -93
  316. package/dist/utils/sdk-package-loader.test.d.ts +0 -2
  317. package/dist/utils/sdk-package-loader.test.d.ts.map +0 -1
  318. package/dist/utils/sdk-package-loader.test.js +0 -84
  319. package/dist/utils/validation.test.d.ts +0 -2
  320. package/dist/utils/validation.test.d.ts.map +0 -1
  321. package/dist/utils/validation.test.js +0 -88
  322. package/dist/utils/yaml-utils.test.d.ts +0 -2
  323. package/dist/utils/yaml-utils.test.d.ts.map +0 -1
  324. package/dist/utils/yaml-utils.test.js +0 -363
@@ -3,6 +3,7 @@
3
3
  * 提供 MCP Tools 供 AI Agent 调用
4
4
  */
5
5
  import fs from "node:fs";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -10,7 +11,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
10
11
  import { AgentClient } from "./agent-client.js";
11
12
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION, REGISTER_MAX_RETRIES, REGISTER_RETRY_INTERVAL_MS, DEGRADED_MODE_ENABLED, DEGRADED_MODE_RETRY_INTERVAL_MS, DEGRADED_MODE_MAX_RETRIES, } from "./constants.js";
12
13
  import { logger } from "./logger.js";
13
- import { MCP_TOOLS } from "./mcp-tools.js";
14
+ import { MCP_TOOLS, MEMORY_TOOL_NAMES } from "./mcp-tools.js";
14
15
  import { MessageBuffer } from "./message-buffer.js";
15
16
  import { ContextManager } from "./context-manager.js";
16
17
  import { MemoryCapacityError, MemoryStore, } from "./memory-store.js";
@@ -23,7 +24,6 @@ const DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS = 120_000;
23
24
  * poll_message 永不超时返回空,只在有消息可返回时才退出。
24
25
  */
25
26
  const POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS = 30_000;
26
- const DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS = 5_000;
27
27
  const PATH_UNSAFE_CHARACTERS = new Set([
28
28
  "<",
29
29
  ">",
@@ -48,6 +48,8 @@ export class McpServer {
48
48
  hasRoleName = false;
49
49
  /** 上下文管理器 (v1.8.0) */
50
50
  contextManager;
51
+ /** 子 Agent 模式:仅暴露 Memory 工具,拒绝非 Memory 工具调用 */
52
+ isSubAgent;
51
53
  /**
52
54
  * 内存级去重缓存:记录本轮已通知过的 listable memory 文件变更状态。
53
55
  * key = memoryId,value = 已通知时刻各绑定文件的 MD5 复合哈希。
@@ -56,13 +58,29 @@ export class McpServer {
56
58
  */
57
59
  notifiedFileChangeMemoryIds = new Map();
58
60
  /**
59
- * 本地缓存 listable 记忆(task_list/recently_memory)的文件 MD5 map。
61
+ * 本地缓存 listable 记忆(recently_memory/agent_memory)的文件 MD5 map。
60
62
  * 服务端(Java 后端)不持久化 file_md5_map 字段,listByType 返回的记录该字段为 undefined。
61
63
  * 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
62
64
  */
63
65
  listableMd5Cache = new Map();
64
- /** 后台 Memory 文件变更检测定时器 (v1.9.0) */
65
- memoryFileCheckInterval = null;
66
+ /**
67
+ * 租约续期定时器 (v1.21.0)。
68
+ * 每 60 秒续期一次当前 agent 持有的所有 updating memory 的租约。
69
+ */
70
+ leaseRenewalTimer = null;
71
+ static LEASE_DURATION_MINUTES = 10;
72
+ static LEASE_RENEW_INTERVAL_MS = 60_000;
73
+ /**
74
+ * claim 去重缓存 (v1.21.0)。
75
+ * 避免 5s 后台定时器重复调 server claim 同一个 memory。
76
+ * key = memory URI, value = 本地缓存的租约到期时间戳。
77
+ * 仅缓存 claim 成功的 URI,在 lease 到期前不再调 server。
78
+ */
79
+ claimedCache = new Map();
80
+ /** syncClaimStatusFromServer 互斥锁,防止后台定时器与 poll 路径并发执行 (v1.21.0) */
81
+ syncClaimMutex = Promise.resolve();
82
+ /** profile 是否已同步(防止 activelySyncProfile 与 profileHandler 双重同步) */
83
+ profileSynced = false;
66
84
  /**
67
85
  * Memory 通知 null 结果缓存:key → expiresAt 时间戳。
68
86
  * 仅缓存 null(无通知)结果,避免 while(true) 每 30s 重复读盘。
@@ -70,6 +88,11 @@ export class McpServer {
70
88
  */
71
89
  memoryNotificationNullCache = new Map();
72
90
  static MEMORY_NOTIFICATION_NULL_TTL_MS = 60_000;
91
+ /**
92
+ * poll_message 单例控制:防止并发调用导致消息被第一个消费后第二个空等。
93
+ * 当已有 poll 在进行时,后续调用直接等待同一个 Promise。
94
+ */
95
+ pendingPollMessage = null;
73
96
  // 系统提示词由 runtime adapter(ACode/OpenCode/Codex/Claude)在会话创建时注入,
74
97
  // 无需通过 MCP 工具返回。三方 runtime(OpenCode/Claude)将三层提示词合并为一条 system prompt,
75
98
  // ACode 则直接注入为前三条 system role 消息。
@@ -78,7 +101,9 @@ export class McpServer {
78
101
  * 当服务端返回的 roleName 与进程初始化时不一致时,重建 roleMemoryStore 指向正确的目录。
79
102
  */
80
103
  syncRoleMemoryStore(profile) {
104
+ logger.info(`[McpServer] syncRoleMemoryStore 被调用: displayName=${profile.displayName}, roleName="${profile.roleName ?? '(空)'}", id=${profile.id}`);
81
105
  if (!profile.roleName) {
106
+ logger.warn(`[McpServer] syncRoleMemoryStore: profile.roleName 为空,hasRoleName 保持 false。profile=${JSON.stringify({ id: profile.id, displayName: profile.displayName, roleName: profile.roleName, instanceName: profile.instanceName })}`);
82
107
  return;
83
108
  }
84
109
  const expectedRoleDir = McpServer.sanitizeRoleDirectoryName(profile.roleName);
@@ -90,6 +115,100 @@ export class McpServer {
90
115
  this.roleMemoryStore = new MemoryStore(McpServer.buildRoleMemoryStorePath(expectedRoleDir));
91
116
  this.initialRoleName = expectedRoleDir;
92
117
  }
118
+ /**
119
+ * 在 profile 到达后检查是否需要将本地永久记忆同步到 server。
120
+ * 触发条件:当前角色在 server 上还没有任何 memory 类型记录(通常发生在重部署后 server DB 为空时)。
121
+ * 主流程:异步执行,不阻塞 profile 处理链路。
122
+ * 1. 调用 server get_memory_stats 获取当前角色 memory 类型计数
123
+ * 2. 若 count === 0,读取本地永久记忆(角色子目录 + 遗留父目录 fallback)
124
+ * 3. 逐条调用 server create_memory 写入(已存在则静默跳过)
125
+ */
126
+ maybeSyncPermanentMemoryToServer() {
127
+ this.doMaybeSyncPermanentMemoryToServer().catch((e) => {
128
+ logger.warn('[McpServer] maybeSyncPermanentMemoryToServer 异常: %s, stack=%s', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : '(no stack)');
129
+ });
130
+ }
131
+ async doMaybeSyncPermanentMemoryToServer() {
132
+ logger.info("[McpServer] maybeSyncPermanentMemoryToServer 开始,initialRoleName="
133
+ + this.initialRoleName);
134
+ // Step 1: 检查 server 当前角色的 memory 计数
135
+ let stats;
136
+ try {
137
+ stats = await this.memoryStore.getStats();
138
+ }
139
+ catch (e) {
140
+ logger.warn("[McpServer] 获取 server 记忆统计失败,跳过同步:", e instanceof Error ? e.message : e);
141
+ return;
142
+ }
143
+ const memoryCount = stats.by_type["project_memory"]?.count ?? 0;
144
+ logger.info(`[McpServer] server memory 计数=${memoryCount},`
145
+ + `是否需要同步=${memoryCount === 0}`);
146
+ if (memoryCount > 0) {
147
+ return;
148
+ }
149
+ // Step 2: 读取本地永久记忆
150
+ // 2a: 先读角色子目录
151
+ let localMemories = [];
152
+ try {
153
+ localMemories = await this.roleMemoryStore.listByType("project_memory");
154
+ logger.info(`[McpServer] 角色子目录读取到 ${localMemories.length} 条永久记忆`);
155
+ }
156
+ catch (e) {
157
+ logger.warn("[McpServer] 读取角色子目录永久记忆失败:", e instanceof Error ? e.message : e);
158
+ }
159
+ // 2b: 角色子目录为空时,fallback 读遗留父目录(旧版无角色子目录)
160
+ if (localMemories.length === 0) {
161
+ const legacyStorePath = path.dirname(McpServer.buildRoleMemoryStorePath(this.initialRoleName));
162
+ logger.info(`[McpServer] 尝试从遗留目录读取: ${legacyStorePath}`);
163
+ try {
164
+ const legacyStore = new MemoryStore(legacyStorePath);
165
+ const legacyMemories = await legacyStore.listByType("project_memory");
166
+ if (legacyMemories.length > 0) {
167
+ logger.info(`[McpServer] 从遗留目录读取到 ${legacyMemories.length} 条永久记忆`);
168
+ localMemories = legacyMemories;
169
+ }
170
+ }
171
+ catch (e) {
172
+ logger.warn("[McpServer] 读取遗留目录永久记忆失败:", e instanceof Error ? e.message : e);
173
+ }
174
+ }
175
+ if (localMemories.length === 0) {
176
+ logger.info("[McpServer] 本地也无永久记忆,跳过同步");
177
+ return;
178
+ }
179
+ // Step 3: 逐条写入 server
180
+ let synced = 0;
181
+ let skipped = 0;
182
+ for (const record of localMemories) {
183
+ try {
184
+ await this.memoryStore.createMemory({
185
+ memory_type: "project_memory",
186
+ uri: record.uri,
187
+ title: record.title,
188
+ content: record.content,
189
+ keywords: record.keywords,
190
+ bound_files: record.bound_files,
191
+ file_md5_map: record.file_md5_map,
192
+ last_check_time: record.last_check_time,
193
+ });
194
+ synced++;
195
+ logger.info(`[McpServer] 同步永久记忆成功: ${record.uri}`);
196
+ }
197
+ catch (e) {
198
+ const msg = e instanceof Error ? e.message : String(e);
199
+ if (msg.includes("already exists")
200
+ || msg.includes("Memory already exists")) {
201
+ skipped++;
202
+ }
203
+ else {
204
+ logger.warn(`[McpServer] 同步永久记忆失败 (${record.uri}): ${msg}`);
205
+ skipped++;
206
+ }
207
+ }
208
+ }
209
+ logger.info(`[McpServer] 永久记忆同步完成: 成功 ${synced}, 跳过 ${skipped}, 共 ${localMemories.length}`);
210
+ await this.reportCombinedMemoryStats();
211
+ }
93
212
  /**
94
213
  * 规范化绑定的文件路径。
95
214
  * 项目约定以下前缀都表示工作区根路径:
@@ -179,7 +298,7 @@ export class McpServer {
179
298
  keywords: record.keywords,
180
299
  bound_files: record.bound_files,
181
300
  file_md5_map: record.file_md5_map,
182
- update_status: record.update_status,
301
+ memory_status: record.memory_status,
183
302
  updating_agent_id: record.updating_agent_id,
184
303
  last_check_time: record.last_check_time,
185
304
  };
@@ -194,6 +313,7 @@ export class McpServer {
194
313
  this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
195
314
  this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
196
315
  this.memoryStoresShareFile = false;
316
+ this.isSubAgent = process.env.AWS_SUB_AGENT === "true";
197
317
  this.contextManager = new ContextManager(this.sessionContextMemoryStore);
198
318
  this.server = new Server({
199
319
  name: MCP_SERVER_NAME,
@@ -207,7 +327,10 @@ export class McpServer {
207
327
  }
208
328
  setupHandlers() {
209
329
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
210
- return { tools: MCP_TOOLS };
330
+ const tools = this.isSubAgent
331
+ ? MCP_TOOLS.filter((t) => MEMORY_TOOL_NAMES.has(t.name))
332
+ : MCP_TOOLS;
333
+ return { tools };
211
334
  });
212
335
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
213
336
  const { name, arguments: args } = request.params;
@@ -230,6 +353,8 @@ export class McpServer {
230
353
  ...error.details,
231
354
  }
232
355
  : { error: errorMessage };
356
+ // 服务端记录完整异常,方便排查 LOST 等异常状态
357
+ logger.error('[McpServer] 工具调用异常: tool=%s, error=%s, stack=%s', name, errorMessage, error instanceof Error ? error.stack : '(no stack)');
233
358
  return {
234
359
  content: [
235
360
  {
@@ -246,24 +371,44 @@ export class McpServer {
246
371
  * 处理工具调用
247
372
  */
248
373
  async handleToolCall(name, args) {
374
+ // 子 agent 共享父 agent MCP 连接时注入的标记:
375
+ // 剥离后跳过状态上报,避免子 agent 事件出现在父 agent 时间线上
376
+ const isSubAgentCall = args?._aws_sub_agent === true;
377
+ // 委托等待循环中后续 poll_message 注入的标记:
378
+ // 跳过 reportToolUse/reportToolResult,只保留 reportThinking,
379
+ // 避免"调用MCP↔思考中"状态反复跳动
380
+ const isSilentPoll = args?._aws_silent_poll === true;
381
+ const cleanArgs = { ...args };
382
+ delete cleanArgs._aws_sub_agent;
383
+ delete cleanArgs._aws_silent_poll;
249
384
  // 报告工具调用状态
250
- const statusReporter = this.agentClient.getStatusReporter();
251
- if (statusReporter) {
252
- statusReporter.reportToolUse(name, this.sanitizeToolUseArgs(name, args));
385
+ // - agent 调用:完全跳过(不污染父 agent 时间线)
386
+ // - 静默 poll:跳过 reportToolUse(不报"调用MCP"),但仍保留 statusReporter 供 reportThinking
387
+ const statusReporter = isSubAgentCall
388
+ ? null
389
+ : this.agentClient.getStatusReporter();
390
+ if (statusReporter && !isSilentPoll) {
391
+ statusReporter.reportToolUse(name, this.sanitizeToolUseArgs(name, cleanArgs));
253
392
  }
254
393
  try {
255
- const result = await this.executeToolCall(name, args);
394
+ const result = await this.executeToolCall(name, cleanArgs);
256
395
  // 工具调用完成后,先上报返回数据供时间线日志查看,再切换为思考状态
257
396
  // (Agent 正在处理工具结果、决定下一步动作,而非真正的空闲等待)
258
- if (statusReporter) {
259
- statusReporter.reportToolResult(result);
260
- statusReporter.reportThinking();
397
+ if (statusReporter && !isSilentPoll) {
398
+ const reportableResult = this.stripAutoDelegateForReporting(result);
399
+ if (reportableResult !== null) {
400
+ // 正常结果:上报 toolResult + thinking(工具调用完成,回到思考状态)
401
+ statusReporter.reportToolResult(reportableResult);
402
+ statusReporter.reportThinking();
403
+ }
404
+ // 委托消息(reportableResult === null):跳过 reportToolResult 和 reportThinking
405
+ // ACode runtime 会重新 poll_message,工具调用未真正结束,保持"调用MCP"状态
261
406
  }
262
407
  return result;
263
408
  }
264
409
  catch (error) {
265
410
  // 工具调用失败,报告错误状态
266
- if (statusReporter) {
411
+ if (statusReporter && !isSilentPoll) {
267
412
  statusReporter.reportToolError(error);
268
413
  }
269
414
  throw error;
@@ -273,6 +418,9 @@ export class McpServer {
273
418
  * 执行具体的工具调用
274
419
  */
275
420
  async executeToolCall(name, args) {
421
+ if (this.isSubAgent && !MEMORY_TOOL_NAMES.has(name)) {
422
+ throw new Error(`Sub-agent is not allowed to call tool: ${name}`);
423
+ }
276
424
  switch (name) {
277
425
  case "get_colleague":
278
426
  return await this.agentClient.discoverColleagues();
@@ -281,8 +429,6 @@ export class McpServer {
281
429
  case "get_profile": {
282
430
  return await this.agentClient.getProfile();
283
431
  }
284
- case "get_status":
285
- return this.agentClient.getState();
286
432
  case "hire_colleague":
287
433
  return await this.handleHireColleague(args);
288
434
  case "hire_colleagues_batch":
@@ -329,8 +475,6 @@ export class McpServer {
329
475
  return await this.handleListMemories(args);
330
476
  case "get_memory_stats":
331
477
  return await this.handleGetMemoryStats(args);
332
- case "archive_recently_memory":
333
- return await this.handleArchiveRecentlyMemory(args);
334
478
  // Memory 增强功能工具 (v1.6.0)
335
479
  case "list_memories_by_file":
336
480
  return await this.handleListMemoriesByFile(args);
@@ -341,23 +485,15 @@ export class McpServer {
341
485
  return await this.handleMemoryRecurrence(args);
342
486
  case "memory_recurrence_complete":
343
487
  return await this.handleMemoryRecurrenceComplete();
344
- // Memory 短期记忆 TODO 完成工具 (v1.14.0)
345
- case "todo_completed":
346
- return await this.handleTodoCompleted(args);
347
- // 上下文压缩工具 (v1.8.0)
348
- case "get_context_stats":
349
- return await this.handleGetContextStats(args);
350
- case "compact_context":
351
- return await this.handleCompactContext(args);
352
488
  default:
353
489
  throw new Error(`Unknown tool: ${name}`);
354
490
  }
355
491
  }
356
492
  // ============ 工具处理方法 ============
357
493
  static MEMORY_TYPES = [
358
- "task_list",
359
494
  "recently_memory",
360
- "memory",
495
+ "agent_memory",
496
+ "project_memory",
361
497
  "session_context",
362
498
  ];
363
499
  static MEMORY_TOOL_NAMES = new Set([
@@ -368,15 +504,12 @@ export class McpServer {
368
504
  "search_memory",
369
505
  "list_memories",
370
506
  "get_memory_stats",
371
- "archive_recently_memory",
372
507
  // Memory 增强功能工具 (v1.6.0)
373
508
  "list_memories_by_file",
374
509
  "abandon_memory_update",
375
510
  // Memory 近期记忆过期与回忆工具 (v1.11.0)
376
511
  "memory_recurrence",
377
512
  "memory_recurrence_complete",
378
- // Memory 短期记忆 TODO 完成工具 (v1.14.0)
379
- "todo_completed",
380
513
  ]);
381
514
  /**
382
515
  * 从 startPath 向上逐级查找 .agentswork 目录。
@@ -427,7 +560,7 @@ export class McpServer {
427
560
  }
428
561
  /**
429
562
  * 解析记忆存储路径。
430
- * 主流程:task_list/recently_memory 走 server 数据库;长期 memory 默认写入项目 .agentswork/memory/memory/<roleName>/<domain>.json。
563
+ * 主流程:recently_memory/agent_memory 走 server 数据库;长期 project_memory 默认写入项目 .agentswork/memory/memory/<roleName>/<domain>.json。
431
564
  */
432
565
  static resolveMemoryStorePaths() {
433
566
  const configuredPath = String(process.env.AWS_MEMORY_STORE_PATH || "").trim();
@@ -483,16 +616,16 @@ export class McpServer {
483
616
  }
484
617
  /**
485
618
  * 按记忆生命周期选择存储作用域。
486
- * 主流程:长期 memory 走角色级 store;task_list/recently_memory 走当前 Agent 实例级 store。
619
+ * 主流程:长期 project_memory 走角色级 store;recently_memory/agent_memory 走当前 Agent 实例级 store。
487
620
  */
488
621
  /**
489
622
  * 按记忆生命周期选择存储作用域。
490
- * 主流程:永久记忆(memory)仅在 hasRoleName 为 true 时走角色级 store,
491
- * 否则回退到实例级 server store(避免空角色名下所有实例共享 default-role 目录导致跨角色污染)。
492
- * task_list/recently_memory/session_context 始终走实例级 store。
623
+ * 主流程:永久记忆(project_memory)始终走本地 roleMemoryStore(.agentswork),
624
+ * 确保本地文件始终有副本,后续部署恢复时可通过 Local Server 同步回 server。
625
+ * recently_memory/agent_memory/session_context 始终走实例级 server store。
493
626
  */
494
627
  getMemoryStoreForType(memoryType) {
495
- if (memoryType === "memory" && this.hasRoleName) {
628
+ if (memoryType === "project_memory") {
496
629
  return this.roleMemoryStore;
497
630
  }
498
631
  return this.memoryStore;
@@ -506,17 +639,32 @@ export class McpServer {
506
639
  return await this.memoryStore.readMemory(input);
507
640
  }
508
641
  if (input.memory_type) {
509
- return await this.getMemoryStoreForType(input.memory_type).readMemory(input);
510
- }
511
- // hasRoleName 为 false 时不查 roleMemoryStore,避免空角色名共享 default-role 目录
512
- if (this.hasRoleName) {
513
- const longTermRecord = await this.roleMemoryStore.readMemory({
514
- ...input,
515
- memory_type: "memory",
516
- });
517
- if (longTermRecord) {
518
- return longTermRecord;
642
+ const localStore = this.getMemoryStoreForType(input.memory_type);
643
+ const localRecord = await localStore.readMemory(input);
644
+ if (localRecord) {
645
+ return localRecord;
646
+ }
647
+ // project_memory 跨实例可见性:本地未命中时回退服务端角色级 scope,
648
+ // 使兄弟实例创建的记忆也能被读取(文件变更通知后 agent 需 read_memory)。
649
+ if (input.memory_type === "project_memory") {
650
+ return await this.memoryStore.readMemory(input);
519
651
  }
652
+ return null;
653
+ }
654
+ const longTermRecord = await this.roleMemoryStore.readMemory({
655
+ ...input,
656
+ memory_type: "project_memory",
657
+ });
658
+ if (longTermRecord) {
659
+ return longTermRecord;
660
+ }
661
+ // 本地角色级未命中,回退服务端角色级 project_memory(兄弟实例创建的记忆)
662
+ const serverLongTerm = await this.memoryStore.readMemory({
663
+ ...input,
664
+ memory_type: "project_memory",
665
+ });
666
+ if (serverLongTerm) {
667
+ return serverLongTerm;
520
668
  }
521
669
  return await this.memoryStore.readMemory(input);
522
670
  }
@@ -529,19 +677,32 @@ export class McpServer {
529
677
  return { store: this.memoryStore, memory_type: input.memory_type };
530
678
  }
531
679
  if (input.memory_type) {
532
- return {
533
- store: this.getMemoryStoreForType(input.memory_type),
534
- memory_type: input.memory_type,
535
- };
536
- }
537
- if (this.hasRoleName) {
538
- const longTermRecord = await this.roleMemoryStore.readMemory({
539
- uri: input.uri,
540
- memory_type: "memory",
541
- });
542
- if (longTermRecord) {
543
- return { store: this.roleMemoryStore, memory_type: "memory" };
680
+ const localStore = this.getMemoryStoreForType(input.memory_type);
681
+ // project_memory 跨实例:本地有则用本地,否则用服务端角色级 scope
682
+ // (兄弟实例创建的记忆仅在服务端可见,需路由到 server 才能 update/delete)
683
+ if (input.memory_type === "project_memory") {
684
+ const localRecord = await localStore.readMemory(input);
685
+ if (localRecord) {
686
+ return { store: localStore, memory_type: "project_memory" };
687
+ }
688
+ return { store: this.memoryStore, memory_type: "project_memory" };
544
689
  }
690
+ return { store: localStore, memory_type: input.memory_type };
691
+ }
692
+ const longTermRecord = await this.roleMemoryStore.readMemory({
693
+ uri: input.uri,
694
+ memory_type: "project_memory",
695
+ });
696
+ if (longTermRecord) {
697
+ return { store: this.roleMemoryStore, memory_type: "project_memory" };
698
+ }
699
+ // 回退服务端角色级 project_memory(兄弟实例创建的记忆)
700
+ const serverLongTerm = await this.memoryStore.readMemory({
701
+ uri: input.uri,
702
+ memory_type: "project_memory",
703
+ });
704
+ if (serverLongTerm) {
705
+ return { store: this.memoryStore, memory_type: "project_memory" };
545
706
  }
546
707
  return { store: this.memoryStore };
547
708
  }
@@ -558,17 +719,17 @@ export class McpServer {
558
719
  // permanentCount 取两者的较大值作为保守估计(下界)。
559
720
  // 注意: 当 instance 和 role 分别有对方没有的唯一 URI 时,实际并集 > Math.max,
560
721
  // 但此处只有 count 无 URI 列表,无法精确计算并集,使用下界避免面板显示虚高。
561
- const permanentCount = Math.max(instanceStats.by_type.memory.count, roleStats.by_type.memory.count);
562
- // total 同样保守估算:用 server 非 memory count + permanentCount 下界
563
- const serverNonMemoryTotal = instanceStats.total - instanceStats.by_type.memory.count;
722
+ const permanentCount = Math.max(instanceStats.by_type.project_memory.count, roleStats.by_type.project_memory.count);
723
+ // total 同样保守估算:用 server 非 project_memory count + permanentCount 下界
724
+ const serverNonMemoryTotal = instanceStats.total - instanceStats.by_type.project_memory.count;
564
725
  return {
565
726
  total: serverNonMemoryTotal + permanentCount,
566
727
  listable: instanceStats.listable,
567
728
  by_type: {
568
- task_list: instanceStats.by_type.task_list,
569
729
  recently_memory: instanceStats.by_type.recently_memory,
570
- memory: {
571
- ...roleStats.by_type.memory,
730
+ agent_memory: instanceStats.by_type.agent_memory,
731
+ project_memory: {
732
+ ...roleStats.by_type.project_memory,
572
733
  count: permanentCount,
573
734
  },
574
735
  session_context: instanceStats.by_type.session_context,
@@ -591,44 +752,6 @@ export class McpServer {
591
752
  logger.warn("[McpServer] 上报合并 memory 统计失败,已保留本地记忆写入结果:", error);
592
753
  }
593
754
  }
594
- /**
595
- * 同步本地永久记忆到服务端。
596
- * 主流程:读取本地 roleMemoryStore 中所有 memory 类型记录 -> 逐条写入服务端 memoryStore -> 已存在的记录静默跳过。
597
- */
598
- async syncLocalPermanentMemoryToServer() {
599
- if (!this.hasRoleName)
600
- return;
601
- try {
602
- const memories = await this.roleMemoryStore.listByType("memory");
603
- if (memories.length === 0)
604
- return;
605
- let syncedCount = 0;
606
- for (const memory of memories) {
607
- try {
608
- await this.memoryStore.createMemory({
609
- memory_type: "memory",
610
- uri: memory.uri,
611
- title: memory.title,
612
- content: memory.content,
613
- keywords: memory.keywords,
614
- bound_files: memory.bound_files,
615
- });
616
- syncedCount++;
617
- }
618
- catch (e) {
619
- // If already exists on server, skip
620
- const errMsg = e instanceof Error ? e.message : String(e);
621
- if (!errMsg.includes("already exists")) {
622
- logger.warn(`[McpServer] Failed to sync permanent memory ${memory.uri}:`, e);
623
- }
624
- }
625
- }
626
- logger.info(`[McpServer] Synced ${syncedCount}/${memories.length} permanent memories to server`);
627
- }
628
- catch (error) {
629
- logger.error("[McpServer] Failed to sync permanent memories to server:", error);
630
- }
631
- }
632
755
  /**
633
756
  * 系统提示词由 runtime adapter 在会话创建时注入,MCP 层面不再负责返回。
634
757
  * 详见 startListeningInternal -> profileHandler 中的合并注入逻辑。
@@ -665,6 +788,43 @@ export class McpServer {
665
788
  }
666
789
  return sanitized;
667
790
  }
791
+ /**
792
+ * 过滤工具返回结果中的 _acodeAutoDelegate 消息,避免自动委托通知
793
+ * (memory 文件变更/过期/容量整理)作为工具返回数据泄露到时间线。
794
+ *
795
+ * 背景:ACode 运行时在 processMemoryAutoDelegates 中拦截这些消息并 spawn
796
+ * 子 agent 处理,主 agent 不会感知。但 MCP server 的 statusReporter 在
797
+ * 拦截之前就已上报原始结果,Java 后端 AgentEventService 会将含 actionResult
798
+ * 的状态事件持久化并通过 AGENT_EVENTS_DELTA 推送到 dashboard 时间线,
799
+ * 导致本应被隐藏的委托通知暴露给用户。
800
+ *
801
+ * 返回 null 表示结果全部为委托消息,应跳过 reportToolResult
802
+ * (由 ACode 运行时在拦截后上报最终结果)。
803
+ */
804
+ stripAutoDelegateForReporting(result) {
805
+ if (!result || typeof result !== "object") {
806
+ return result;
807
+ }
808
+ const pollResult = result;
809
+ const directMessages = Array.isArray(pollResult.directMessages)
810
+ ? pollResult.directMessages
811
+ : [];
812
+ const groupMessages = Array.isArray(pollResult.groupMessages)
813
+ ? pollResult.groupMessages
814
+ : [];
815
+ const isDelegate = (m) => !!m &&
816
+ typeof m === "object" &&
817
+ "_acodeAutoDelegate" in m;
818
+ const hasDelegate = directMessages.some(isDelegate);
819
+ if (!hasDelegate) {
820
+ return result;
821
+ }
822
+ const remainingDm = directMessages.filter((m) => !isDelegate(m));
823
+ if (remainingDm.length === 0 && groupMessages.length === 0) {
824
+ return null;
825
+ }
826
+ return { ...pollResult, directMessages: remainingDm };
827
+ }
668
828
  /**
669
829
  * 创建本地记忆。
670
830
  * 主流程:校验必填字段 -> 交给 MemoryStore 规范化 URI 和类型 -> 返回不含 content 的写入结果。
@@ -698,16 +858,16 @@ export class McpServer {
698
858
  const created = await this.getMemoryStoreForType(input.memory_type).createMemory(input);
699
859
  await this.reportCombinedMemoryStats();
700
860
  // Dual-write permanent memory to server
701
- if (input.memory_type === "memory") {
861
+ if (input.memory_type === "project_memory") {
702
862
  try {
703
- await this.memoryStore.createMemory(input).catch(() => { });
863
+ await this.memoryStore.createMemory(input);
704
864
  }
705
865
  catch (e) {
706
866
  logger.warn("[McpServer] Failed to sync permanent memory to server:", e);
707
867
  }
708
868
  }
709
- // v1.13.0: recently_memory 过期判定、task_list todo 状态由服务端 memory_status 字段管理,
710
- // 无需 TS 侧本地缓存。服务端默认按类型填充正确状态(recently_memory='normal', task_list='pending')。
869
+ // v1.13.0: recently_memory 过期判定、agent_memory 过期/回忆状态由服务端 memory_status 字段管理,
870
+ // 无需 TS 侧本地缓存。服务端默认按类型填充正确状态(recently_memory='normal', agent_memory='normal')。
711
871
  return this.toMemoryWriteResult(created);
712
872
  }
713
873
  /**
@@ -723,7 +883,9 @@ export class McpServer {
723
883
  }
724
884
  /**
725
885
  * 更新本地记忆。
726
- * 主流程:要求至少一种变更方式 -> MemoryStore 执行精确替换/追加和版本递增 -> 返回不含 content 的写入结果。
886
+ * 主流程:精确替换/追加/仅状态更新 -> MemoryStore 执行变更和版本递增 -> 返回不含 content 的写入结果。
887
+ * 支持三种写法:1) 精确替换(old_string + new_string);2) 追加(append);3) 无内容更新(仅传 uri,
888
+ * 用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景)。
727
889
  */
728
890
  async handleUpdateMemory(args) {
729
891
  const hasReplace = typeof args.old_string === "string" ||
@@ -734,10 +896,9 @@ export class McpServer {
734
896
  if (hasReplace && !hasCompleteReplace) {
735
897
  throw new Error("old_string and new_string must be provided together");
736
898
  }
737
- if (!hasCompleteReplace &&
738
- (typeof append !== "string" || append.length === 0)) {
739
- throw new Error("Provide either old_string/new_string or append");
740
- }
899
+ // 允许无内容更新:当未提供 old_string/new_string 且未提供有效 append 时,
900
+ // 视为"仅状态更新"——用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景。
901
+ // 此时内容不变,但会重算绑定文件 MD5 并续期(expirable 类型)。
741
902
  const input = {
742
903
  uri: this.getRequiredString(args, "uri"),
743
904
  memory_type: this.getOptionalMemoryType(args),
@@ -779,27 +940,35 @@ export class McpServer {
779
940
  });
780
941
  await this.reportCombinedMemoryStats();
781
942
  // Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
782
- if (target.memory_type === "memory") {
943
+ // target.store 已是服务端(兄弟实例创建的记忆回退场景)时跳过,避免重复写
944
+ if (target.memory_type === "project_memory" && target.store !== this.memoryStore) {
783
945
  try {
784
- await this.memoryStore
785
- .updateMemory({
946
+ await this.memoryStore.updateMemory({
786
947
  ...input,
787
- memory_type: "memory",
948
+ memory_type: "project_memory",
788
949
  file_md5_map: fileMd5Map,
789
950
  last_check_time: lastCheckTime,
790
- })
791
- .catch(() => { });
951
+ });
792
952
  }
793
953
  catch (e) {
794
954
  logger.warn("[McpServer] Failed to sync permanent memory update to server:", e);
795
955
  }
796
956
  }
797
- // v1.13.0: task_list TODO 状态、recently_memory 过期与回忆状态均由服务端 memory_status 字段管理。
798
- // 显式传入的 todo_status / recall_status / memory_status 已在 updateMemory 内部透传到服务端。
799
- // 服务端会自动处理"所有 TODO 已完成时清理全部已完成项"的逻辑(见 AgentMemoryService.updateMemory)。
800
- // 内容变更对 recently_memory 的"续期"语义现在由过期判定基于 created_at 决定,
801
- // update_memory 内容变更不会重置 created_at,因此"续期"实际对应"修改 created_at" — 当前不提供该能力,
802
- // 如需续期建议使用 archive_recently_memory 归档或重新 createMemory 替代。
957
+ // v1.13.0: agent_memory 过期与回忆状态由服务端 memory_status 字段管理。
958
+ // v1.21.0: 如果是 project_memory 且之前是 updating 状态,释放 Server claim
959
+ if (target.memory_type === 'project_memory' && existingForMd5?.memory_status === 'updating') {
960
+ try {
961
+ await this.memoryStore.releaseMemoryClaim(input.uri);
962
+ this.claimedCache.delete(input.uri);
963
+ // 所有 claim 已释放,停止续期定时器避免无意义请求
964
+ if (this.claimedCache.size === 0) {
965
+ this.stopLeaseRenewal();
966
+ }
967
+ }
968
+ catch (e) {
969
+ logger.warn('[McpServer] 释放 memory claim 失败: uri=%s, error=%s', input.uri, e instanceof Error ? e.message : String(e));
970
+ }
971
+ }
803
972
  return this.toMemoryWriteResult(updated);
804
973
  }
805
974
  /**
@@ -819,14 +988,13 @@ export class McpServer {
819
988
  });
820
989
  if (deleted) {
821
990
  await this.reportCombinedMemoryStats();
822
- // v1.13.0: 服务端 deleteMemory 内部已处理"task_list 删除后清空 completed 残留"逻辑,
991
+ // v1.13.0: 服务端 deleteMemory 内部已处理相关清理逻辑,
823
992
  // TS 侧无需任何本地元数据清理。
824
993
  // Dual-write permanent memory delete to server
825
- if (effectiveType === "memory") {
994
+ // target.store 已是服务端时跳过,避免重复删除
995
+ if (effectiveType === "project_memory" && target.store !== this.memoryStore) {
826
996
  try {
827
- await this.memoryStore
828
- .deleteMemory({ uri: input.uri, memory_type: "memory" })
829
- .catch(() => { });
997
+ await this.memoryStore.deleteMemory({ uri: input.uri, memory_type: "project_memory" });
830
998
  }
831
999
  catch (e) {
832
1000
  logger.warn("[McpServer] Failed to sync permanent memory delete to server:", e);
@@ -872,7 +1040,7 @@ export class McpServer {
872
1040
  }
873
1041
  /**
874
1042
  * 搜索自己工作区的所有记忆。
875
- * 具体逻辑:按 memory_type 分流 -> 无类型时合并 roleMemoryStore(永久)与 memoryStore(task_list/recently)。
1043
+ * 具体逻辑:按 memory_type 分流 -> 无类型时合并 roleMemoryStore(永久)与 memoryStore(recently/agent)。
876
1044
  */
877
1045
  async searchOwnMemories(input) {
878
1046
  if (input.memory_type) {
@@ -886,7 +1054,7 @@ export class McpServer {
886
1054
  return await this.memoryStore.searchMemory(input);
887
1055
  }
888
1056
  const [roleResult, instanceResult] = await Promise.all([
889
- this.roleMemoryStore.searchMemory({ ...input, memory_type: "memory" }),
1057
+ this.roleMemoryStore.searchMemory({ ...input, memory_type: "project_memory" }),
890
1058
  this.memoryStore.searchMemory(input),
891
1059
  ]);
892
1060
  const seen = new Set();
@@ -926,7 +1094,7 @@ export class McpServer {
926
1094
  }
927
1095
  /**
928
1096
  * 列出短期类型记忆。
929
- * 主流程:只允许 task_list/recently_memory,由 MemoryStore 阻止长期 memory 的全量列出。
1097
+ * 主流程:只允许 recently_memory/agent_memory/project_memory,由 MemoryStore 阻止 session_context 的全量列出。
930
1098
  */
931
1099
  async handleListMemories(args) {
932
1100
  return await this.memoryStore.listByType(this.getRequiredString(args, "memory_type"));
@@ -952,58 +1120,39 @@ export class McpServer {
952
1120
  },
953
1121
  };
954
1122
  }
955
- /**
956
- * 归档近期记忆。
957
- * 主流程:将 recently_memory 原地转换为长期 memory,并递增版本号。
958
- */
959
- async handleArchiveRecentlyMemory(args) {
960
- const uri = this.getRequiredString(args, "uri");
961
- if (this.memoryStoresShareFile) {
962
- return await this.memoryStore.archiveRecentlyMemory(uri);
963
- }
964
- const record = await this.memoryStore.readMemory({
965
- uri,
966
- memory_type: "recently_memory",
967
- });
968
- if (!record) {
969
- throw new Error(`Memory not found: ${uri}`);
970
- }
971
- const archived = await this.getMemoryStoreForType("memory").createMemory({
972
- memory_type: "memory",
973
- uri: record.uri,
974
- title: record.title,
975
- content: record.content,
976
- keywords: record.keywords,
977
- bound_files: record.bound_files,
978
- });
979
- await this.memoryStore.deleteMemory({
980
- uri: record.uri,
981
- memory_type: "recently_memory",
982
- });
983
- await this.reportCombinedMemoryStats();
984
- return archived;
985
- }
986
1123
  /**
987
1124
  * 按文件查询绑定了该文件的 Memory
988
1125
  * Memory 增强功能 (v1.6.0)
989
1126
  */
990
1127
  /**
991
1128
  * 按文件路径查询 Memory(支持 scope 跨 Agent 搜索)。
992
- * 主流程:解析scope -> own(默认)仅自己 -> children通过server按path_prefix匹配子Agent -> all合并。
993
- * 对于永久记忆(type=memory)同时查询本地roleMemoryStore作为补充。
1129
+ * 主流程:解析scope -> own(默认)仅自己 -> children搜索子Agent -> all合并。
1130
+ * 子 Agent 搜索同时走两条路径:
1131
+ * 1. server searchChildByFilePath(查 DB)
1132
+ * 2. 本地文件系统扫描 child .agentswork/memory/memory/(查本地永久记忆文件)
1133
+ * 两条路径结果按 id 去重合并,确保无论子 Agent 的永久记忆是否已同步到
1134
+ * 服务端数据库,父 Agent 都能搜索到。
994
1135
  */
995
1136
  async handleListMemoriesByFile(args) {
996
1137
  const rawPath = this.getRequiredString(args, "file_path");
997
1138
  const normalizedPath = this.normalizeBoundFilePath(rawPath);
998
1139
  const memoryType = this.getOptionalMemoryType(args);
999
1140
  const rawScope = this.getOptionalString(args, "scope");
1000
- const scope = rawScope === "children" || rawScope === "all" ? rawScope : "own";
1001
- // scope='children': 仅通过 server 搜索子 Agent(带 path_prefix 自动匹配)
1141
+ // 默认 scope='all':合并搜索自身与子Agent绑定文件的记忆
1142
+ const scope = rawScope === "own" || rawScope === "children" ? rawScope : "all";
1143
+ // 搜索子 Agent 的 memory(server + 本地文件系统双路径,并行)
1144
+ const fetchChildMemories = async () => {
1145
+ const [serverResult, localResult] = await Promise.all([
1146
+ this.memoryStore.listByFile(normalizedPath, {
1147
+ memory_type: memoryType,
1148
+ scope: "children",
1149
+ }).catch(() => []), // server 不可用时降级
1150
+ this.searchChildMemoriesByFileLocal(normalizedPath, memoryType),
1151
+ ]);
1152
+ return this.mergeIdDedupe(serverResult, localResult);
1153
+ };
1002
1154
  if (scope === "children") {
1003
- const childResult = await this.memoryStore.listByFile(normalizedPath, {
1004
- memory_type: memoryType,
1005
- scope: "children",
1006
- });
1155
+ const childResult = await fetchChildMemories();
1007
1156
  return {
1008
1157
  file_path: normalizedPath,
1009
1158
  count: childResult.length,
@@ -1019,11 +1168,8 @@ export class McpServer {
1019
1168
  memories: ownMemories.map(m => this.toMemoryRecordWithMetadata(m)),
1020
1169
  };
1021
1170
  }
1022
- // scope='all': 合并自己的 + 子 Agent
1023
- const childResult = await this.memoryStore.listByFile(normalizedPath, {
1024
- memory_type: memoryType,
1025
- scope: "children",
1026
- });
1171
+ // scope='all': 合并自己的 + 子 Agent 的(server + 本地双路径已由 fetchChildMemories 聚合)
1172
+ const childResult = await fetchChildMemories();
1027
1173
  const seen = new Set(ownMemories.map(m => m.id));
1028
1174
  const merged = [...ownMemories];
1029
1175
  for (const m of childResult) {
@@ -1039,6 +1185,135 @@ export class McpServer {
1039
1185
  memories: merged.map(m => this.toMemoryRecordWithMetadata(m)),
1040
1186
  };
1041
1187
  }
1188
+ /**
1189
+ * 按 id 去重合并两批 MemoryRecord,按 updatedAt 倒序排列。
1190
+ * 首批(left)优先级高,其 id 会标记为 seen,重复的 right 项被丢弃。
1191
+ */
1192
+ mergeIdDedupe(left, right) {
1193
+ const seen = new Set();
1194
+ for (const m of left) {
1195
+ if (m.id)
1196
+ seen.add(m.id);
1197
+ }
1198
+ const merged = [...left];
1199
+ for (const m of right) {
1200
+ if (m.id && !seen.has(m.id)) {
1201
+ merged.push(m);
1202
+ seen.add(m.id);
1203
+ }
1204
+ }
1205
+ merged.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1206
+ return merged;
1207
+ }
1208
+ /**
1209
+ * 通过文件系统扫描子 Agent 的 .agentswork 目录,
1210
+ * 读取永久记忆 JSON 文件,按 path_prefix 匹配 bound_files。
1211
+ *
1212
+ * 主流程:
1213
+ * 1. 解析 file_path(如 ./frontend/src/main.ts)的路径层级
1214
+ * 2. 逐级(从长到短)找到子 Agent 的 .agentswork 目录
1215
+ * 3. 读取该目录下所有 role 的 JSON 文件
1216
+ * 4. 对每条记忆,将 bound_files 与 child-relative 路径比较
1217
+ *
1218
+ * 作为 server searchChildByFilePath 的本地补充,
1219
+ * 确保子 Agent 的本地永久记忆(未同步到 server)也能被搜索到。
1220
+ */
1221
+ async searchChildMemoriesByFileLocal(filePath, memoryType) {
1222
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
1223
+ const normalizedFilePath = this.normalizeBoundFilePath(filePath);
1224
+ // 解析路径 segments
1225
+ // 例如 ./frontend/src/main.ts → relativePath = "frontend/src/main.ts"
1226
+ // segments = ["frontend", "src", "main.ts"]
1227
+ const relativePath = normalizedFilePath.startsWith('./')
1228
+ ? normalizedFilePath.substring(2)
1229
+ : normalizedFilePath;
1230
+ const segments = relativePath.split('/');
1231
+ const results = [];
1232
+ const seenById = new Set();
1233
+ const { access, readdir, readFile } = await import('node:fs/promises');
1234
+ // 从最长的路径前缀开始尝试(兼容嵌套子 Agent:深度=2 时 prefix=frontend/src)
1235
+ for (let depth = segments.length - 1; depth >= 1; depth--) {
1236
+ const prefixSegments = segments.slice(0, depth);
1237
+ const childRelativeSegments = segments.slice(depth);
1238
+ const childRelativePath = './' + childRelativeSegments.join('/');
1239
+ // 子 Agent 的候选工作区路径
1240
+ const childWorkspace = path.join(workspacePath, ...prefixSegments);
1241
+ // 跳过父 Agent 自身(避免把 parent 当 child 扫)
1242
+ if (childWorkspace === workspacePath)
1243
+ continue;
1244
+ const childAgentsworkDir = path.join(childWorkspace, '.agentswork');
1245
+ // 检查 .agentswork 是否存在
1246
+ try {
1247
+ await access(childAgentsworkDir);
1248
+ }
1249
+ catch {
1250
+ continue; // 这个前缀不是子 Agent 工作区
1251
+ }
1252
+ // 读取子 Agent 的永久记忆目录 .agentswork/memory/memory/<roleName>/
1253
+ const memoryBaseDir = path.join(childAgentsworkDir, 'memory', 'memory');
1254
+ let roleDirs;
1255
+ try {
1256
+ const entries = await readdir(memoryBaseDir, { withFileTypes: true });
1257
+ roleDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
1258
+ }
1259
+ catch {
1260
+ continue;
1261
+ }
1262
+ for (const roleDir of roleDirs) {
1263
+ const rolePath = path.join(memoryBaseDir, roleDir);
1264
+ let files;
1265
+ try {
1266
+ files = await readdir(rolePath);
1267
+ }
1268
+ catch {
1269
+ continue;
1270
+ }
1271
+ for (const file of files) {
1272
+ if (!file.endsWith('.json'))
1273
+ continue;
1274
+ const jsonPath = path.join(rolePath, file);
1275
+ let content;
1276
+ try {
1277
+ content = await readFile(jsonPath, 'utf-8');
1278
+ }
1279
+ catch {
1280
+ continue;
1281
+ }
1282
+ let parsed;
1283
+ try {
1284
+ parsed = JSON.parse(content);
1285
+ }
1286
+ catch {
1287
+ continue;
1288
+ }
1289
+ if (!parsed.by_uri)
1290
+ continue;
1291
+ // 遍历该 domain 的所有记忆,匹配 bound_files
1292
+ for (const [uri, record] of Object.entries(parsed.by_uri)) {
1293
+ if (!record)
1294
+ continue;
1295
+ const recordId = record.id || uri;
1296
+ if (seenById.has(recordId))
1297
+ continue;
1298
+ if (memoryType && record.memory_type !== memoryType)
1299
+ continue;
1300
+ if (record.bound_files && record.bound_files.length > 0) {
1301
+ const matched = record.bound_files.some(bf => this.normalizeBoundFilePath(bf) === childRelativePath);
1302
+ if (matched) {
1303
+ seenById.add(recordId);
1304
+ results.push({
1305
+ ...record,
1306
+ uri: record.uri || uri,
1307
+ });
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+ results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1315
+ return results;
1316
+ }
1042
1317
  /**
1043
1318
  * 按文件路径搜索本 Agent 的记忆。
1044
1319
  * 同时查询临时记忆(server)和永久记忆(local roleMemoryStore)。
@@ -1049,8 +1324,8 @@ export class McpServer {
1049
1324
  });
1050
1325
  let localResult = [];
1051
1326
  // 补充查询本地永久记忆(server 可能未同步);无角色名时跳过(避免 default-role 跨角色污染)
1052
- if (this.hasRoleName && (!memoryType || memoryType === "memory")) {
1053
- localResult = await this.roleMemoryStore.listByFile(filePath, "memory");
1327
+ if (this.hasRoleName && (!memoryType || memoryType === "project_memory")) {
1328
+ localResult = await this.roleMemoryStore.listByFile(filePath, "project_memory");
1054
1329
  }
1055
1330
  // 去重合并
1056
1331
  const seen = new Set(serverResult.map(m => `${m.memory_type}:${m.uri}`));
@@ -1065,38 +1340,54 @@ export class McpServer {
1065
1340
  return merged;
1066
1341
  }
1067
1342
  /**
1068
- * 放弃 Memory 更新状态
1343
+ * 放弃 Memory 更新状态 (v1.21.0 改为走 release API)
1069
1344
  * Memory 增强功能 (v1.6.0)
1070
1345
  */
1071
1346
  async handleAbandonMemoryUpdate(args) {
1072
1347
  const uri = this.getRequiredString(args, "uri");
1073
1348
  const memoryType = this.getOptionalMemoryType(args);
1349
+ // 调 Server release API 释放 claim
1350
+ await this.memoryStore.releaseMemoryClaim(uri);
1351
+ // 同时清除本地 roleMemoryStore 的 updating 状态(如果是 project_memory)
1352
+ // 兄弟实例创建的记忆本地不存在时静默跳过,服务端 release 才是权威操作
1074
1353
  const record = await this.memoryStore.readMemory({ uri, memory_type: memoryType });
1075
- if (!record) {
1076
- throw new Error(`Memory not found: ${uri}`);
1354
+ if (record?.memory_type === 'project_memory') {
1355
+ try {
1356
+ await this.roleMemoryStore.updateMemory({
1357
+ uri,
1358
+ memory_type: 'project_memory',
1359
+ });
1360
+ // 不传 memory_status → MemoryStore 默认重置为 normal
1361
+ }
1362
+ catch (e) {
1363
+ logger.info(`[McpServer] abandon_memory_update: 本地无此记忆,跳过本地状态清理: ${uri}`);
1364
+ }
1365
+ }
1366
+ // 从去重缓存移除
1367
+ this.claimedCache.delete(uri);
1368
+ // 所有 claim 已释放,停止续期定时器避免无意义请求
1369
+ if (this.claimedCache.size === 0) {
1370
+ this.stopLeaseRenewal();
1077
1371
  }
1078
- // 清除 updating 状态
1079
- await this.memoryStore.updateMemory({
1080
- uri: record.uri,
1081
- memory_type: record.memory_type,
1082
- });
1083
1372
  return {
1084
1373
  success: true,
1085
- uri: record.uri,
1374
+ uri,
1086
1375
  message: "Memory 更新状态已清除",
1087
1376
  };
1088
1377
  }
1089
- // ============ Memory 近期记忆过期与回忆工具处理 (v1.11.0 / v1.13.0) ============
1378
+ // ============ Memory 近期记忆过期与回忆工具处理 (v1.11.0 / v1.13.0 / v1.19.0) ============
1090
1379
  // v1.13.0: 过期判定与 recalling 状态标记已统一迁移到服务端 memory_status 字段,
1091
1380
  // 下方两个工具方法直接调用服务端接口,不再依赖任何本地元数据。
1381
+ // v1.19.0: recently_memory 纳入过期回忆流程,与 agent_memory 行为完全一致。
1092
1382
  /**
1093
1383
  * 处理 memory_recurrence 工具调用。
1094
- * 主流程:调用服务端 list_expired_memories 拉取已过期 recently_memory ->
1384
+ * 主流程:调用服务端 list_expired_memories 拉取已过期的 recently_memory / agent_memory ->
1095
1385
  * 按过期时间排序(最早优先)-> 取前 limit 条 ->
1096
1386
  * 仅标记这 limit 条为「回忆中」状态 -> 返回这些记忆及剩余数量。
1097
1387
  */
1098
1388
  async handleMemoryRecurrence(args) {
1099
1389
  // v1.13.0: 过期判定与 recalling 状态全部由服务端管理
1390
+ // v1.19.0: recently_memory 同样纳入过期回忆流程
1100
1391
  const limit = Math.max(1, args?.limit ?? 10);
1101
1392
  const expiredMemories = await this.memoryStore.listExpiredRecentlyMemories();
1102
1393
  if (expiredMemories.length === 0) {
@@ -1122,7 +1413,7 @@ export class McpServer {
1122
1413
  await this.memoryStore.markExpiredMemoriesRecalling(batchUris);
1123
1414
  }
1124
1415
  catch (e) {
1125
- logger.warn("[McpServer] 标记过期记忆为 recalling 失败:", e);
1416
+ logger.warn('[McpServer] 标记过期记忆为 recalling 失败: uriCount=%s, error=%s', batchUris.length, e instanceof Error ? e.message : String(e));
1126
1417
  }
1127
1418
  const remaining = Math.max(0, expiredMemories.length - batch.length);
1128
1419
  // 返回过期记忆的摘要(不含完整 content 以减少上下文占用)
@@ -1131,7 +1422,6 @@ export class McpServer {
1131
1422
  title: m.title,
1132
1423
  domain: m.domain,
1133
1424
  path: m.path,
1134
- recall_status: 'recalling',
1135
1425
  memory_status: 'recalling',
1136
1426
  contentPreview: typeof m.content === 'string' && m.content.length > 200 ? m.content.substring(0, 200) + '...' : m.content,
1137
1427
  createdAt: m.createdAt,
@@ -1145,10 +1435,10 @@ export class McpServer {
1145
1435
  totalExpired: expiredMemories.length,
1146
1436
  message: remaining > 0
1147
1437
  ? `本次处理 ${batch.length} 条即将遗忘的记忆(最早过期优先),剩余 ${remaining} 条待处理。\n` +
1148
- `请审查后决定(archive_recently_memory 归档 / update_memory 续期),\n` +
1438
+ `请审查后决定(update_memory 续期,TTL ×2 / 不处理),\n` +
1149
1439
  `完成后调用 memory_recurrence_complete 清理,再调 memory_recurrence 处理下一批。`
1150
1440
  : `已处理全部 ${batch.length} 条即将遗忘的记忆。\n` +
1151
- `请审查后决定(archive_recently_memory 归档 / update_memory 续期),\n` +
1441
+ `请审查后决定(update_memory 续期,TTL ×2 / 不处理),\n` +
1152
1442
  `完成后调用 memory_recurrence_complete 清理。`,
1153
1443
  };
1154
1444
  }
@@ -1163,7 +1453,7 @@ export class McpServer {
1163
1453
  result = await this.memoryStore.clearRecallingExpiredMemories();
1164
1454
  }
1165
1455
  catch (e) {
1166
- logger.warn("[McpServer] 清理回忆中记忆失败:", e);
1456
+ logger.warn('[McpServer] 清理回忆中记忆失败: %s', e instanceof Error ? e.message : String(e));
1167
1457
  }
1168
1458
  await this.reportCombinedMemoryStats();
1169
1459
  const deletedCount = result.deleted ?? 0;
@@ -1175,104 +1465,6 @@ export class McpServer {
1175
1465
  : `已清理 ${deletedCount} 条已过期近期记忆。`,
1176
1466
  };
1177
1467
  }
1178
- // ============ 短期记忆 TODO 完成工具处理 (v1.14.0) ============
1179
- /**
1180
- * 标记 task_list 短期记忆为已完成。
1181
- * 主流程:调用 memoryStore.todoCompleted(uri) -> 标记完成 -> 检查全部完成 -> 自动清理 -> 返回结果。
1182
- */
1183
- async handleTodoCompleted(args) {
1184
- const result = await this.memoryStore.todoCompleted(args.uri);
1185
- await this.reportCombinedMemoryStats();
1186
- const allCleanedMsg = result.allCleaned
1187
- ? `所有 TODO 均已完成,已自动清理 ${result.cleanedCount} 条已完成短期记忆。`
1188
- : "已标记为已完成。";
1189
- return {
1190
- ...result,
1191
- message: allCleanedMsg,
1192
- };
1193
- }
1194
- // ============ 上下文压缩工具处理 (v1.8.0) ============
1195
- /**
1196
- * 获取当前会话的上下文统计。
1197
- * 主流程:从 profile 获取模型名称 -> 通过模型名查找上下文窗口 -> 调用 ContextManager 获取统计 -> 返回。
1198
- */
1199
- async handleGetContextStats(args) {
1200
- const profile = await this.agentClient.getProfile().catch(() => null);
1201
- const sessionId = args.sessionId ?? profile?.runtimeSessionId ?? "default";
1202
- const modelName = profile?.prompt ? this.guessModelName(profile) : "unknown";
1203
- const modelInfo = {
1204
- provider: modelName.split("/")[0] || "unknown",
1205
- model: modelName,
1206
- contextLimit: 0, // ContextManager 内部会通过 lookupModelLimits 查找
1207
- };
1208
- const stats = await this.contextManager.getStats(sessionId, modelInfo);
1209
- // 如果 contextLimit 仍为 0,用查找表的默认值
1210
- if (stats.contextLimit === 0) {
1211
- const limits = this.contextManager.lookupModelLimits(modelName);
1212
- return { ...stats, contextLimit: limits.contextLimit, remaining: limits.contextLimit - stats.totalUsed, usagePercent: Math.round((stats.totalUsed / limits.contextLimit) * 100) };
1213
- }
1214
- return stats;
1215
- }
1216
- /**
1217
- * 从 profile 猜测当前使用的模型名称。
1218
- * 主流程:读取 prompt 中的模型信息 -> 回退到 runtimeCliCommand 中的提示 -> 最终回退到环境变量或默认值。
1219
- */
1220
- guessModelName(profile) {
1221
- // 尝试从 prompt 中提取模型名(prompt 可能包含 @model 标记)
1222
- const promptMatch = profile.prompt?.match(/@model\s+(\S+)/i);
1223
- if (promptMatch?.[1])
1224
- return promptMatch[1];
1225
- // 回退到 CLI 命令中的线索
1226
- if (profile.runtimeMode)
1227
- return profile.runtimeMode;
1228
- // 默认值
1229
- return process.env.AWS_DEFAULT_MODEL || "gpt-4o";
1230
- }
1231
- /**
1232
- * 处理上下文压缩请求。
1233
- * 主流程:解析策略 -> 获取消息历史 -> 执行压缩 -> 保存结果 -> 返回。
1234
- */
1235
- async handleCompactContext(args) {
1236
- const profile = await this.agentClient.getProfile().catch(() => null);
1237
- const sessionId = profile?.runtimeSessionId ?? "default";
1238
- const strategy = args.strategy ?? "auto";
1239
- const keepLastTurns = args.keepLastTurns ?? 3;
1240
- // 获取消息历史
1241
- const messages = await this.fetchRecentMessages();
1242
- switch (strategy) {
1243
- case "prune": {
1244
- const result = this.contextManager.prune(messages, keepLastTurns);
1245
- await this.contextManager.saveCompactionResult(sessionId, result);
1246
- return result;
1247
- }
1248
- case "summarize": {
1249
- // summarize 需要 Agent 自己调用 LLM 来完成
1250
- // 这里先执行 prune 释放部分空间,然后返回需要 Agent 进行 summarize
1251
- const pruneResult = this.contextManager.prune(messages, keepLastTurns);
1252
- await this.contextManager.saveCompactionResult(sessionId, pruneResult);
1253
- return {
1254
- ...pruneResult,
1255
- strategy: "summarize",
1256
- summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
1257
- };
1258
- }
1259
- case "auto":
1260
- default: {
1261
- // 先尝试 prune
1262
- const pruneResult = this.contextManager.prune(messages, keepLastTurns);
1263
- if (pruneResult.freedTokens >= 20_000) {
1264
- await this.contextManager.saveCompactionResult(sessionId, pruneResult);
1265
- return pruneResult;
1266
- }
1267
- // prune 释放不够,回退到 summarize
1268
- return {
1269
- ...pruneResult,
1270
- strategy: "summarize",
1271
- summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
1272
- };
1273
- }
1274
- }
1275
- }
1276
1468
  /**
1277
1469
  * 获取最近的消息历史用于压缩。
1278
1470
  * 主流程:获取未读消息 + 历史消息 -> 合并排序 -> 返回。
@@ -1382,70 +1574,122 @@ export class McpServer {
1382
1574
  return result;
1383
1575
  }
1384
1576
  async handlePollMessage() {
1385
- // 首次非阻塞预取:先拉一次已有消息填充缓冲区
1577
+ logger.info('[McpServer] handlePollMessage 被调用');
1578
+ if (this.pendingPollMessage) {
1579
+ logger.info('[McpServer] poll_message 已有进行中的调用,等待其完成');
1580
+ return await this.pendingPollMessage;
1581
+ }
1582
+ this.pendingPollMessage = (async () => {
1583
+ return await this.doPollMessage();
1584
+ })();
1585
+ try {
1586
+ return await this.pendingPollMessage;
1587
+ }
1588
+ finally {
1589
+ this.pendingPollMessage = null;
1590
+ }
1591
+ }
1592
+ /**
1593
+ * poll_message 主循环逻辑。
1594
+ *
1595
+ * 紧急/非紧急分类策略:
1596
+ * - 紧急类:未读消息(私信/群聊)、task、todo
1597
+ * - 非紧急类:memory过期回忆、memory文件变更、memory容量整理
1598
+ * - 规则:有紧急类时返回所有命中的紧急项(不搭配非紧急类);
1599
+ * 无紧急类时返回所有命中的非紧急项。
1600
+ *
1601
+ * 返回组合(只可能是以下两种之一):
1602
+ * 1、所有命中的紧急项(未读消息 + todo + task 中命中的全部)
1603
+ * 2、所有命中的非紧急项(memory过期 + 文件变更 + 容量整理 中命中的全部)
1604
+ *
1605
+ * 注:subAgent 终态事件由 ACode 运行时层(runtime.ts callPollMessageWithSubAgentRace)
1606
+ * 在 MCP 调用前后注入,MCP server 内部不直接检测子 agent 状态。
1607
+ * 注:Todo 在入口和 while 循环中均检查,确保阻塞期间创建的 Todo 也能被感知。
1608
+ */
1609
+ async doPollMessage() {
1610
+ logger.info('[McpServer] doPollMessage 被调用');
1611
+ // 紧急类检查:hydrate 未读消息 + task
1386
1612
  await this.hydrateUnreadMessagesFromServer({
1387
1613
  blockIfEmpty: false,
1388
- includePipelineTasks: false,
1614
+ includePipelineTasks: true,
1389
1615
  });
1390
- // poll_message 永不超时返回空:while(true) 内部每 30s 轮询一次
1391
- // WebSocket 消息(主动通知)→ 实时响应
1392
- // Memory 状态变化(无法主动通知)→ 最长 30s 延迟
1393
- while (true) {
1394
- // 1. 检查 WebSocket 缓冲区(来自 push + 后台补拉)
1395
- const buffered = this.messageBuffer.get();
1396
- if (buffered.directMessages.length > 0 || buffered.groupMessages.length > 0) {
1397
- return await this.finalizeWithMemoryNotifications(buffered);
1398
- }
1399
- // 2. 检查 Memory 通知(缓冲区无消息时直接返回)
1400
- if (!this.memoryFileCheckInterval) {
1401
- const fileChange = await this.buildMemoryFileChangeNotification();
1402
- if (fileChange) {
1403
- return this.buildPollResult({ directMessages: [fileChange], groupMessages: [] });
1404
- }
1616
+ // 紧急类检查:Todo(只检查一次)
1617
+ const todoNotif = await this.buildTodoReminderNotification();
1618
+ // 紧急类:有未读消息/task 时,加上 todo(如果有),返回所有紧急项
1619
+ const urgentBuffered = this.messageBuffer.get();
1620
+ if (urgentBuffered.directMessages.length > 0 || urgentBuffered.groupMessages.length > 0) {
1621
+ if (todoNotif) {
1622
+ urgentBuffered.directMessages.unshift(todoNotif);
1405
1623
  }
1406
- const recurrence = await this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification());
1407
- if (recurrence) {
1408
- return this.buildPollResult({ directMessages: [recurrence], groupMessages: [] });
1624
+ return this.buildPollResult(urgentBuffered);
1625
+ }
1626
+ // 紧急类:无未读消息/task 但有 todo,直接返回 todo
1627
+ if (todoNotif) {
1628
+ return this.buildPollResult({ directMessages: [todoNotif], groupMessages: [] });
1629
+ }
1630
+ // 非紧急类:无紧急类时,检查所有非紧急项(memory过期 + 文件变更 + 容量整理)
1631
+ const nonUrgentMessages = await this.checkAllLocalNonUrgentNotifications();
1632
+ if (nonUrgentMessages.length > 0) {
1633
+ return this.buildPollResult({ directMessages: nonUrgentMessages, groupMessages: [] });
1634
+ }
1635
+ // 无紧急类且无非紧急类:进入 while 循环等待新消息
1636
+ while (true) {
1637
+ const polled = await this.messageBuffer.poll(POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS);
1638
+ // 紧急类:poll 到消息(私信/群聊),立即返回
1639
+ if (polled.directMessages.length > 0 || polled.groupMessages.length > 0) {
1640
+ return this.buildPollResult(polled);
1409
1641
  }
1410
- const todo = await this.getCachedMemoryNotification('todo', () => this.buildMemoryTodoNotification());
1411
- if (todo) {
1412
- return this.buildPollResult({ directMessages: [todo], groupMessages: [] });
1642
+ // poll 超时无消息时,重新检查紧急类(task + todo 可能在此期间到达/创建)
1643
+ // 与入口处逻辑一致:先查 todo,再合并消息/task,紧急项一起返回
1644
+ await this.hydrateUnreadMessagesFromServer({
1645
+ blockIfEmpty: false,
1646
+ includePipelineTasks: true,
1647
+ });
1648
+ const todoNotifLoop = await this.buildTodoReminderNotification();
1649
+ const taskBuffered = this.messageBuffer.get();
1650
+ if (taskBuffered.directMessages.length > 0 || taskBuffered.groupMessages.length > 0) {
1651
+ if (todoNotifLoop) {
1652
+ taskBuffered.directMessages.unshift(todoNotifLoop);
1653
+ }
1654
+ return this.buildPollResult(taskBuffered);
1413
1655
  }
1414
- const cleanup = await this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification());
1415
- if (cleanup) {
1416
- return this.buildPollResult({ directMessages: [cleanup], groupMessages: [] });
1656
+ // 紧急类:无消息/task 但有 todo,返回 todo
1657
+ if (todoNotifLoop) {
1658
+ return this.buildPollResult({ directMessages: [todoNotifLoop], groupMessages: [] });
1417
1659
  }
1418
- // 3. 带超时等 WebSocket 消息(内部轮询周期)
1419
- // poll() 一旦返回就已消费 buffer,不可 `continue` 回 get()(空)
1420
- const polled = await this.messageBuffer.poll(POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS);
1421
- if (polled.directMessages.length > 0 || polled.groupMessages.length > 0) {
1422
- return await this.finalizeWithMemoryNotifications(polled);
1660
+ // 非紧急类:无紧急类时,检查所有非紧急项
1661
+ const nonUrgent = await this.checkAllLocalNonUrgentNotifications();
1662
+ if (nonUrgent.length > 0) {
1663
+ return this.buildPollResult({ directMessages: nonUrgent, groupMessages: [] });
1423
1664
  }
1424
- // 超时 → 继续循环,重新检查 Memory 和缓冲区
1425
1665
  }
1426
1666
  }
1427
1667
  /**
1428
- * 后检 Memory 通知并注入到待返回的消息中。按优先级从高到低注入:
1429
- * 文件变更(始终注入)→ 近期记忆过期回忆(触发即返回)→ TODO(触发即返回)→ 容量整理(始终注入)
1430
- */
1431
- async finalizeWithMemoryNotifications(messages) {
1432
- if (!this.memoryFileCheckInterval) {
1433
- await this.prependMemoryNotification(messages, () => this.buildMemoryFileChangeNotification());
1434
- }
1435
- if (await this.prependMemoryNotification(messages, () => this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification()))) {
1436
- return this.buildPollResult(messages);
1437
- }
1438
- if (await this.prependMemoryNotification(messages, () => this.getCachedMemoryNotification('todo', () => this.buildMemoryTodoNotification()))) {
1439
- return this.buildPollResult(messages);
1440
- }
1441
- await this.prependMemoryNotification(messages, () => this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification()));
1442
- return this.buildPollResult(messages);
1668
+ * 检查本地非紧急通知(memory过期回忆、文件变更、容量整理),返回所有命中的通知。
1669
+ * Task 不在此方法中检查,因为 Task 通过服务端 hydrate 以消息形式返回。
1670
+ */
1671
+ async checkAllLocalNonUrgentNotifications() {
1672
+ const messages = [];
1673
+ // Memory 过期回忆
1674
+ const recurrenceNotif = await this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification());
1675
+ if (recurrenceNotif)
1676
+ messages.push(recurrenceNotif);
1677
+ // Memory 文件变更(需要更新)
1678
+ const fileChangeNotif = await this.buildMemoryFileChangeNotification();
1679
+ if (fileChangeNotif)
1680
+ messages.push(fileChangeNotif);
1681
+ // Memory 容量整理
1682
+ const cleanupNotif = await this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification());
1683
+ if (cleanupNotif)
1684
+ messages.push(cleanupNotif);
1685
+ return messages;
1443
1686
  }
1444
- buildPollResult(messages) {
1687
+ buildPollResult(messages, fromBuffer = true) {
1445
1688
  return {
1446
1689
  directMessages: messages.directMessages,
1447
1690
  groupMessages: messages.groupMessages,
1448
- fromBuffer: true,
1691
+ fromBuffer,
1692
+ systemMessages: [],
1449
1693
  };
1450
1694
  }
1451
1695
  /**
@@ -1495,8 +1739,8 @@ export class McpServer {
1495
1739
  const cleanupActions = [
1496
1740
  "请调用 get_memory_stats 查看容量详情",
1497
1741
  "调用 list_memories 按类型查看超限记忆",
1498
- "删除不再需要的 task_list",
1499
- "删除或 archive_recently_memory 归档 recently_memory 为长期 memory",
1742
+ "删除不再需要的 recently_memory",
1743
+ "更新 agent_memory 续期(TTL ×2)或删除过期项",
1500
1744
  ].join(";");
1501
1745
  return {
1502
1746
  msgId: `memory-cleanup-${Date.now()}`,
@@ -1507,22 +1751,27 @@ export class McpServer {
1507
1751
  requireReply: false,
1508
1752
  prompt: "这是一条本地 memory 容量整理提醒,自动生成,无需持久化。请优先整理已超过建议上限的短期/近期 memory 类型,或具体类型下的 memory 条目。",
1509
1753
  timestamp: new Date().toISOString(),
1754
+ _acodeAutoDelegate: {
1755
+ type: 'memory_cleanup',
1756
+ uris: exceededDomains.map(d => `${d.memoryType}/${d.domain}`),
1757
+ },
1510
1758
  };
1511
1759
  }
1512
1760
  catch (error) {
1513
- logger.warn("[McpServer] 构建本地 memory 整理提醒失败:", error);
1761
+ logger.warn('[McpServer] 构建本地 memory 整理提醒失败: %s', error instanceof Error ? error.message : String(error));
1514
1762
  return null;
1515
1763
  }
1516
1764
  }
1517
1765
  /**
1518
1766
  * 构建近期记忆过期回忆通知(有即将遗忘的记忆需要整理)。
1519
- * Memory 近期记忆过期 (v1.11.0) / v1.13.0 改用服务端 memory_status 字段
1767
+ * Memory 近期记忆过期 (v1.11.0) / v1.13.0 改用服务端 memory_status 字段 / v1.19.0 扩展
1520
1768
  *
1521
1769
  * 通知策略(基于服务端 memory_status):
1522
- * 1. 有过期 recently_memory 且 memory_status='recalling'(回忆中但未完成疏理):
1770
+ * 1. 有过期 recently_memory / agent_memory 且 memory_status='recalling'(回忆中但未完成疏理):
1523
1771
  * 通知 "存在待疏理的即将遗忘记忆,请通过 memory_recurrence 获取进行整理"
1524
- * 2. 有过期 recently_memory 且 memory_status='normal'(尚未开始回忆):
1772
+ * 2. 有过期 recently_memory / agent_memory 且 memory_status='normal'(尚未开始回忆):
1525
1773
  * 通知 "有即将遗忘记忆得整理,请通过 memory_recurrence 进行回忆"
1774
+ * v1.19.0: recently_memory 纳入过期回忆流程,与 agent_memory 一并处理。
1526
1775
  */
1527
1776
  async buildMemoryRecurrenceNotification() {
1528
1777
  try {
@@ -1533,7 +1782,7 @@ export class McpServer {
1533
1782
  const agentId = this.getCurrentAgentId();
1534
1783
  if (!agentId)
1535
1784
  return null;
1536
- const recallingExpired = expiredMemories.filter(m => m.recall_status === 'recalling' || m.memory_status === 'recalling');
1785
+ const recallingExpired = expiredMemories.filter(m => m.memory_status === 'recalling');
1537
1786
  const normalExpired = expiredMemories.filter(m => !recallingExpired.includes(m));
1538
1787
  // 场景1: 有 recalling 状态的过期记忆(Agent 未完成疏理)
1539
1788
  if (recallingExpired.length > 0) {
@@ -1547,6 +1796,10 @@ export class McpServer {
1547
1796
  requireReply: false,
1548
1797
  prompt: "memory_recurrence 取回的记忆仍在回忆中,请通过 memory_recurrence_complete 完成疏理,或继续归档/续期。",
1549
1798
  timestamp: new Date().toISOString(),
1799
+ _acodeAutoDelegate: {
1800
+ type: 'memory_expiration',
1801
+ uris: recallingExpired.map(m => m.uri),
1802
+ },
1550
1803
  };
1551
1804
  }
1552
1805
  // 场景2: 有待回忆的过期记忆
@@ -1561,12 +1814,16 @@ export class McpServer {
1561
1814
  requireReply: false,
1562
1815
  prompt: `这是一条近期记忆过期提醒,共有 ${normalExpired.length} 条记忆即将遗忘,请调用 memory_recurrence 分批次获取并决定处理方式。`,
1563
1816
  timestamp: new Date().toISOString(),
1817
+ _acodeAutoDelegate: {
1818
+ type: 'memory_expiration',
1819
+ uris: normalExpired.map(m => m.uri),
1820
+ },
1564
1821
  };
1565
1822
  }
1566
1823
  return null;
1567
1824
  }
1568
1825
  catch (error) {
1569
- logger.warn("[McpServer] 构建近期记忆过期回忆通知失败:", error);
1826
+ logger.warn('[McpServer] 构建近期记忆过期回忆通知失败: %s', error instanceof Error ? error.message : String(error));
1570
1827
  return null;
1571
1828
  }
1572
1829
  }
@@ -1575,24 +1832,38 @@ export class McpServer {
1575
1832
  * Memory 增强功能 (v1.6.0)
1576
1833
  *
1577
1834
  * 检测流程:
1835
+ * 0. 同步 Server 的 claim 状态到本地,清理过期 claim
1578
1836
  * 1. 检查本 Agent 标记为 updating 但未处理的 Memory
1579
1837
  * 2. 检查短期记忆、近期记忆绑定的文件是否变更
1580
- * 3. 如果没有变更,检查永久记忆的文件变更,标记最多 5 个为 updating
1838
+ * 3. 检查永久记忆的文件变更,先走 Server 原子 claim,再标记本地
1581
1839
  */
1582
1840
  async buildMemoryFileChangeNotification() {
1841
+ logger.info(`[McpServer] buildMemoryFileChangeNotification 被调用: hasRoleName=${this.hasRoleName}, agentId=${this.getCurrentAgentId() ?? '(空)'}`);
1583
1842
  // 无角色名时不检测本地永久记忆文件变更(避免 default-role 跨角色污染)
1584
- if (!this.hasRoleName)
1843
+ if (!this.hasRoleName) {
1844
+ logger.info('[McpServer] 文件变更检测跳过: hasRoleName=false');
1585
1845
  return null;
1846
+ }
1586
1847
  try {
1587
1848
  const agentId = this.getCurrentAgentId();
1588
1849
  if (!agentId) {
1850
+ logger.info('[McpServer] 文件变更检测跳过: agentId 为空');
1589
1851
  return null;
1590
1852
  }
1591
- // 步骤1: 检查是否有 pendingupdating memories(角色本地永久记忆),
1853
+ // Step 0: 同步 Serverclaim 状态到本地 roleMemoryStore(可选,失败降级)。
1854
+ // 清理那些 Server 上已释放(租约过期或被清理)但本地仍标记为 updating 的记录。
1855
+ // Server API 不可用时降级,依赖本地状态,不影响检测流程。
1856
+ await this.syncClaimStatusFromServer(agentId).catch(e => logger.info("[McpServer] Step 0 同步 claim 状态失败(降级):", e));
1857
+ // 步骤1: 检查是否有 pending 的 updating memories(服务端角色级永久记忆),
1592
1858
  // 同时检测绑定文件是否再次变更(用户可能在 Agent 处理前又改了文件)
1593
- const permanentList = await this.roleMemoryStore.listByType('memory');
1594
- const updatingMemories = permanentList.filter(m => m.update_status === 'updating' && m.updating_agent_id === agentId);
1859
+ // 数据源切换:从服务端角色级 scope 读取(同角色所有实例共享),
1860
+ // 不再用本地 roleMemoryStore(仅本实例可见,兄弟实例无法检测到彼此创建的 memory)。
1861
+ const permanentList = await this.memoryStore.listByType('project_memory');
1862
+ const updatingMemories = permanentList.filter(m => m.memory_status === 'updating' && m.updating_agent_id === agentId);
1595
1863
  if (updatingMemories.length > 0) {
1864
+ // 当前 agent 持有未完成的 updating memory,确保租约续期定时器在运行,
1865
+ // 避免 agent 在更新过程中租约过期被其他实例接管。
1866
+ this.startLeaseRenewal();
1596
1867
  // 对 updating 记忆也检测文件是否再次变更
1597
1868
  const updatingChanges = await this.checkMemoriesFileChanges(updatingMemories);
1598
1869
  if (updatingChanges.length > 0) {
@@ -1600,10 +1871,11 @@ export class McpServer {
1600
1871
  for (const change of updatingChanges) {
1601
1872
  const memory = updatingMemories.find(m => m.id === change.memoryId);
1602
1873
  if (memory) {
1603
- await this.roleMemoryStore.updateMemory({
1874
+ // MD5 回写到服务端角色级 scope(显式传 memory_status:updating 防止 auto-release)
1875
+ await this.memoryStore.updateMemory({
1604
1876
  uri: memory.uri,
1605
- memory_type: 'memory',
1606
- update_status: 'updating',
1877
+ memory_type: 'project_memory',
1878
+ memory_status: 'updating',
1607
1879
  updating_agent_id: agentId,
1608
1880
  file_md5_map: memory.file_md5_map,
1609
1881
  last_check_time: memory.last_check_time,
@@ -1624,12 +1896,17 @@ export class McpServer {
1624
1896
  requireReply: false,
1625
1897
  prompt: "这是一条文件变更提醒,您之前标记为更新中的记忆绑定的文件又有新变更,请处理。",
1626
1898
  timestamp: new Date().toISOString(),
1899
+ _acodeAutoDelegate: {
1900
+ type: 'memory_file_change',
1901
+ uris: updatingChanges.slice(0, 5).map(c => c.memoryUri),
1902
+ },
1627
1903
  };
1628
1904
  }
1629
1905
  const memoryList = updatingMemories
1630
1906
  .slice(0, 5)
1631
1907
  .map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
1632
1908
  .join('\n');
1909
+ logger.info(`[McpServer] 文件变更检测 Step1: ${updatingMemories.length} 个 updating memory 待更新,租约已续期`);
1633
1910
  return {
1634
1911
  msgId: `memory-update-pending-${Date.now()}`,
1635
1912
  senderId: "MCP-System",
@@ -1639,14 +1916,22 @@ export class McpServer {
1639
1916
  requireReply: false,
1640
1917
  prompt: "这是一条更新提醒,您之前已标记这些记忆为更新中,请继续处理。",
1641
1918
  timestamp: new Date().toISOString(),
1919
+ _acodeAutoDelegate: {
1920
+ type: 'memory_file_change',
1921
+ uris: updatingMemories.slice(0, 5).map(m => m.uri),
1922
+ },
1642
1923
  };
1643
1924
  }
1644
1925
  // 步骤2: 检查短期记忆和近期记忆(服务端存储)
1645
- const recentlyMemories = await this.memoryStore.listByType('recently_memory');
1646
- const taskListMemories = await this.memoryStore.listByType('task_list');
1647
- const listableMemories = [...recentlyMemories, ...taskListMemories];
1926
+ const recentlyMemoryMemories = await this.memoryStore.listByType('recently_memory');
1927
+ const agentMemoryMemories = await this.memoryStore.listByType('agent_memory');
1928
+ const listableMemories = [...recentlyMemoryMemories, ...agentMemoryMemories];
1929
+ // 诊断日志:列出 listable memory 的绑定文件和 MD5 基线情况
1930
+ const listableWithFiles = listableMemories.filter(m => m.bound_files && m.bound_files.length > 0);
1931
+ logger.info(`[McpServer] 文件变更检测 Step2: recently=${recentlyMemoryMemories.length}, agent=${agentMemoryMemories.length}, ` +
1932
+ `有绑定文件=${listableWithFiles.length}/${listableMemories.length}。` +
1933
+ `详情: ${listableMemories.map(m => `${m.uri}(bound=${m.bound_files?.length ?? 0},md5=${Object.keys(m.file_md5_map ?? {}).length})`).join('; ') || '无 memory'}`);
1648
1934
  // 用本地 MD5 缓存补全 listable 记忆(服务端可能不持久化 file_md5_map 字段)
1649
- // 确保 checkMemoriesFileChanges 有正确的旧 MD5 做比对而非始终以空字典开始
1650
1935
  for (const m of listableMemories) {
1651
1936
  if (!m.file_md5_map) {
1652
1937
  const cached = this.listableMd5Cache.get(m.id);
@@ -1656,17 +1941,20 @@ export class McpServer {
1656
1941
  }
1657
1942
  }
1658
1943
  const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
1659
- // 使用复合 key(memoryId + 当前各绑定文件 MD5)过滤已通知变更,
1660
- // 同一条记忆文件再次变更(MD5 变化)时仍能触发新通知
1661
1944
  const newListableChanges = listableChanges.filter(c => {
1662
1945
  const memory = listableMemories.find(m => m.id === c.memoryId);
1663
1946
  const md5Map = memory?.file_md5_map;
1664
1947
  const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
1665
- return this.notifiedFileChangeMemoryIds.get(c.memoryId) !== changeKey;
1948
+ const lastNotified = this.notifiedFileChangeMemoryIds.get(c.memoryId);
1949
+ const isNew = lastNotified !== changeKey;
1950
+ if (!isNew) {
1951
+ logger.info(`[McpServer] 文件变更检测 Step2: ${c.memoryUri} 变更被去重吞掉 (changeKey=${changeKey}, lastNotified=${lastNotified})`);
1952
+ }
1953
+ return isNew;
1666
1954
  });
1955
+ logger.info(`[McpServer] 文件变更检测 Step2: 检测到变更=${listableChanges.length}, 去重后=${newListableChanges.length}` +
1956
+ (listableChanges.length > 0 ? ` 变更项: ${listableChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
1667
1957
  if (newListableChanges.length > 0) {
1668
- // 记录当前状态的复合键,下次对比时若 MD5 变化则重新通知
1669
- // 同时将 MD5 map 写入本地缓存,弥补服务端不持久化 file_md5_map 的问题
1670
1958
  for (const c of newListableChanges) {
1671
1959
  const memory = listableMemories.find(m => m.id === c.memoryId);
1672
1960
  const md5Map = memory?.file_md5_map;
@@ -1687,52 +1975,164 @@ export class McpServer {
1687
1975
  requireReply: false,
1688
1976
  prompt: "这是一条文件变更提醒,自动生成,无需持久化。",
1689
1977
  timestamp: new Date().toISOString(),
1978
+ _acodeAutoDelegate: {
1979
+ type: 'memory_file_change',
1980
+ uris: newListableChanges.map(c => c.memoryUri),
1981
+ },
1690
1982
  };
1691
1983
  }
1692
- // 步骤3: 检查永久记忆(角色本地存储)
1693
- const normalPermanentMemories = permanentList.filter(m => m.bound_files && m.bound_files.length > 0 &&
1694
- (m.update_status === 'normal' || !m.update_status));
1695
- const permanentChanges = await this.checkMemoriesFileChanges(normalPermanentMemories);
1984
+ // 步骤3: 检查永久记忆(角色本地存储)— 优先走 Server 原子 claim,失败时降级到本地直接标记
1985
+ const permanentMemoriesWithFiles = permanentList.filter(m => m.bound_files && m.bound_files.length > 0
1986
+ && (m.memory_status === 'normal' || !m.memory_status)
1987
+ && !this.isClaimCached(m.uri));
1988
+ const permanentTotalWithFiles = permanentList.filter(m => m.bound_files && m.bound_files.length > 0);
1989
+ logger.info(`[McpServer] 文件变更检测 Step3: project_memory 总数=${permanentList.length}, ` +
1990
+ `有绑定文件=${permanentTotalWithFiles.length}, 待检测(过滤updating/claimed后)=${permanentMemoriesWithFiles.length}。` +
1991
+ `被过滤原因: ${permanentTotalWithFiles.length - permanentMemoriesWithFiles.length} 项(memory_status=updating 或 isClaimCached)`);
1992
+ const permanentChanges = await this.checkMemoriesFileChanges(permanentMemoriesWithFiles);
1993
+ // 回写无基线 memory 的新 MD5 到服务端(修复前创建的旧数据补基线)
1994
+ const baselineUpdatedMemories = permanentMemoriesWithFiles.filter(m => m.file_md5_map && Object.keys(m.file_md5_map).length > 0
1995
+ && !permanentChanges.some(c => c.memoryId === m.id));
1996
+ if (baselineUpdatedMemories.length > 0) {
1997
+ logger.info(`[McpServer] 文件变更检测 Step3: 回写 ${baselineUpdatedMemories.length} 个无基线 memory 的 MD5 基线到服务端`);
1998
+ for (const m of baselineUpdatedMemories) {
1999
+ try {
2000
+ await this.memoryStore.updateMemory({
2001
+ uri: m.uri,
2002
+ memory_type: 'project_memory',
2003
+ file_md5_map: m.file_md5_map,
2004
+ last_check_time: m.last_check_time,
2005
+ });
2006
+ }
2007
+ catch (e) {
2008
+ logger.info(`[McpServer] 回写基线 MD5 失败: ${m.uri}`, e);
2009
+ }
2010
+ }
2011
+ }
2012
+ logger.info(`[McpServer] 文件变更检测 Step3: 检测到变更=${permanentChanges.length}` +
2013
+ (permanentChanges.length > 0 ? ` 变更项: ${permanentChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
1696
2014
  if (permanentChanges.length > 0) {
1697
2015
  // 最多选取 5 个
1698
2016
  const selectedChanges = permanentChanges.slice(0, 5);
1699
- // 标记为 updating 状态,同时持久化当前 MD5 map,
1700
- // 确保后续检测能正确对比文件变更,而非每次都视为"已变更"
1701
- for (const change of selectedChanges) {
1702
- const memory = permanentList.find(m => m.id === change.memoryId);
1703
- if (memory) {
1704
- await this.roleMemoryStore.updateMemory({
1705
- uri: memory.uri,
1706
- memory_type: 'memory',
1707
- update_status: 'updating',
1708
- updating_agent_id: agentId,
1709
- // checkMemoriesFileChanges 已更新 in-memory 对象中的 MD5 map
1710
- file_md5_map: memory.file_md5_map,
1711
- last_check_time: memory.last_check_time,
1712
- });
2017
+ const urisToClaim = selectedChanges.map(c => c.memoryUri);
2018
+ let claimedViaServer = false;
2019
+ let claimedUris = new Set();
2020
+ try {
2021
+ // Path A: 尝试走 Server 原子 claim
2022
+ const claimResult = await this.memoryStore.claimMemoryUpdate(urisToClaim, McpServer.LEASE_DURATION_MINUTES, 'project_memory');
2023
+ claimedUris = new Set(claimResult.claimed_uris ?? []);
2024
+ claimedViaServer = true;
2025
+ }
2026
+ catch (e) {
2027
+ // Path B: Server API 不可用 → 降级到本地直接标记
2028
+ logger.info("[McpServer] Server claim API 不可用,降级到本地直接标记:", e);
2029
+ claimedUris = new Set(urisToClaim);
2030
+ }
2031
+ if (claimedUris.size > 0) {
2032
+ const claimedChanges = selectedChanges.filter(c => claimedUris.has(c.memoryUri));
2033
+ // 标记本地 roleMemoryStore
2034
+ for (const change of claimedChanges) {
2035
+ const memory = permanentList.find(m => m.id === change.memoryId);
2036
+ if (memory) {
2037
+ // claim 状态由 batchClaimUpdate 在服务端原子写入;此处仅回写新 MD5 基线到服务端角色级 scope,
2038
+ // 显式传 memory_status:updating/updating_agent_id 防止 Java updateMemory auto-release 误释放 claim。
2039
+ await this.memoryStore.updateMemory({
2040
+ uri: memory.uri,
2041
+ memory_type: 'project_memory',
2042
+ memory_status: 'updating',
2043
+ updating_agent_id: agentId,
2044
+ file_md5_map: memory.file_md5_map,
2045
+ last_check_time: memory.last_check_time,
2046
+ });
2047
+ // 加入去重缓存(lease 有效期内不再重复 claim)
2048
+ this.claimedCache.set(memory.uri, Date.now() + McpServer.LEASE_DURATION_MINUTES * 60 * 1000);
2049
+ }
1713
2050
  }
2051
+ // 启动租约续期(仅 Server claim 成功时需要,降级模式不续期)
2052
+ if (claimedViaServer && claimedUris.size > 0) {
2053
+ this.startLeaseRenewal();
2054
+ }
2055
+ const changeDetails = claimedChanges
2056
+ .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
2057
+ .join('\n');
2058
+ return {
2059
+ msgId: `memory-file-change-${Date.now()}`,
2060
+ senderId: "MCP-System",
2061
+ senderName: "系统",
2062
+ receiverId: agentId,
2063
+ content: `检测到以下 ${claimedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
2064
+ requireReply: false,
2065
+ prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
2066
+ timestamp: new Date().toISOString(),
2067
+ _acodeAutoDelegate: {
2068
+ type: 'memory_file_change',
2069
+ uris: claimedChanges.map(c => c.memoryUri),
2070
+ },
2071
+ };
1714
2072
  }
1715
- const changeDetails = selectedChanges
1716
- .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
1717
- .join('\n');
1718
- return {
1719
- msgId: `memory-file-change-${Date.now()}`,
1720
- senderId: "MCP-System",
1721
- senderName: "系统",
1722
- receiverId: agentId,
1723
- content: `检测到以下 ${selectedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
1724
- requireReply: false,
1725
- prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
1726
- timestamp: new Date().toISOString(),
1727
- };
1728
2073
  }
2074
+ logger.info('[McpServer] 文件变更检测完成: 无变更通知产生');
1729
2075
  return null;
1730
2076
  }
1731
2077
  catch (error) {
1732
- logger.warn("[McpServer] 构建 Memory 文件变更检测通知失败:", error);
2078
+ logger.warn('[McpServer] 构建 Memory 文件变更检测通知失败: %s', error instanceof Error ? error.message : String(error));
1733
2079
  return null;
1734
2080
  }
1735
2081
  }
2082
+ /**
2083
+ * Step 0: 同步 Server 的 claim 状态到本地 roleMemoryStore (v1.21.0)。
2084
+ * 查询 Server 上当前 agent 持有的 updating 记忆 URI 列表,
2085
+ * 本地中标记为 updating 但 Server 不持有的 → 重置为 normal。
2086
+ * 处理场景:agent crash 后重启,Server 租约已过期被清理,但本地 JSON 仍卡在 updating。
2087
+ */
2088
+ async syncClaimStatusFromServer(agentId) {
2089
+ // 互斥锁:防止后台定时器与 poll 路径并发执行导致 TOCTOU
2090
+ const previous = this.syncClaimMutex;
2091
+ let release;
2092
+ this.syncClaimMutex = new Promise((resolve) => { release = resolve; });
2093
+ await previous;
2094
+ try {
2095
+ // 从 Server 查询当前 agent 持有的 claimed URIs
2096
+ const serverClaimedUris = await this.memoryStore.getMyClaimedUris();
2097
+ const serverClaimedSet = new Set(serverClaimedUris);
2098
+ // 读取本地 roleMemoryStore 中所有 project_memory
2099
+ const localMemories = await this.roleMemoryStore.listByType('project_memory');
2100
+ const staleLocalClaims = localMemories.filter(m => m.memory_status === 'updating'
2101
+ && m.updating_agent_id === agentId
2102
+ && !serverClaimedSet.has(m.uri));
2103
+ if (staleLocalClaims.length > 0) {
2104
+ logger.info(`[McpServer] Step 0: 清理 ${staleLocalClaims.length} 条本地过期 claim(Server 已释放)`);
2105
+ for (const m of staleLocalClaims) {
2106
+ await this.roleMemoryStore.updateMemory({
2107
+ uri: m.uri,
2108
+ memory_type: 'project_memory',
2109
+ });
2110
+ // 不传 memory_status → MemoryStore 默认重置为 normal
2111
+ }
2112
+ }
2113
+ }
2114
+ catch (e) {
2115
+ // Server 不可用时降级,依赖本地状态
2116
+ logger.warn('[McpServer] Step 0: 同步 claim 状态失败(降级): %s', e instanceof Error ? e.message : String(e));
2117
+ }
2118
+ finally {
2119
+ release();
2120
+ }
2121
+ }
2122
+ /**
2123
+ * 检查指定 URI 是否在 claim 去重缓存内 (v1.21.0)。
2124
+ * 在 lease 有效期内不再重复 claim。
2125
+ */
2126
+ isClaimCached(uri) {
2127
+ const expiry = this.claimedCache.get(uri);
2128
+ if (expiry === undefined)
2129
+ return false;
2130
+ if (expiry <= Date.now()) {
2131
+ this.claimedCache.delete(uri);
2132
+ return false;
2133
+ }
2134
+ return true;
2135
+ }
1736
2136
  /**
1737
2137
  * 检查一组 Memory 的文件变更
1738
2138
  * Memory 增强功能 (v1.6.0)
@@ -1747,25 +2147,47 @@ export class McpServer {
1747
2147
  const changedFiles = [];
1748
2148
  const newMd5Map = {};
1749
2149
  const oldMd5Map = memory.file_md5_map || {};
2150
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
2151
+ const hasNoBaseline = Object.keys(oldMd5Map).length === 0;
1750
2152
  for (const filePath of memory.bound_files) {
1751
2153
  try {
1752
2154
  const currentMd5 = await this.calculateFileMd5(filePath);
1753
2155
  if (currentMd5 === null) {
1754
2156
  // 文件不存在或无法读取
1755
- changedFiles.push(`${filePath} (文件不存在或无法访问)`);
2157
+ const resolvedPath = path.resolve(workspacePath, filePath);
2158
+ logger.info(`[McpServer] MD5 计算返回 null: ${filePath} -> ${resolvedPath} (workspace=${workspacePath})`);
2159
+ if (!hasNoBaseline) {
2160
+ // 已有基线但文件读不到 → 视为变更(文件被删除/移动)
2161
+ changedFiles.push(`${filePath} (文件不存在或无法访问)`);
2162
+ }
1756
2163
  continue;
1757
2164
  }
1758
2165
  newMd5Map[filePath] = currentMd5;
1759
2166
  const oldMd5 = oldMd5Map[filePath];
1760
2167
  if (!oldMd5 || oldMd5 !== currentMd5) {
1761
- changedFiles.push(filePath);
2168
+ if (hasNoBaseline) {
2169
+ // 无基线(修复前创建的旧数据或新建未计算 MD5):静默建立基线,不视为变更
2170
+ logger.info(`[McpServer] 建立基线: ${filePath} new=${currentMd5} (workspace=${workspacePath}, memory=${memory.uri})`);
2171
+ }
2172
+ else {
2173
+ logger.info(`[McpServer] 文件变更: ${filePath} old=${oldMd5} new=${currentMd5} (workspace=${workspacePath})`);
2174
+ changedFiles.push(filePath);
2175
+ }
1762
2176
  }
1763
2177
  }
1764
2178
  catch (error) {
1765
2179
  logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
1766
- changedFiles.push(`${filePath} (MD5计算失败)`);
2180
+ if (!hasNoBaseline) {
2181
+ changedFiles.push(`${filePath} (MD5计算失败)`);
2182
+ }
1767
2183
  }
1768
2184
  }
2185
+ // 无基线时:更新 MD5 map 到 memory 对象(供调用方回写),但不计入变更
2186
+ if (hasNoBaseline && Object.keys(newMd5Map).length > 0) {
2187
+ memory.file_md5_map = newMd5Map;
2188
+ memory.last_check_time = new Date().toISOString();
2189
+ continue; // 不 push 到 changes
2190
+ }
1769
2191
  if (changedFiles.length > 0) {
1770
2192
  changes.push({
1771
2193
  memoryId: memory.id,
@@ -1791,9 +2213,20 @@ export class McpServer {
1791
2213
  try {
1792
2214
  const { createReadStream } = await import('node:fs');
1793
2215
  const { createHash } = await import('node:crypto');
2216
+ // 绑定文件以工作区相对路径存储(./xxx),需相对 AWS_WORKSPACE_PATH 解析,
2217
+ // 与 validateBoundFilesExist 保持一致,避免 cwd ≠ 工作区时读不到文件。
2218
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
2219
+ const resolvedPath = path.resolve(workspacePath, filePath);
2220
+ // L-5: 防止路径逃逸(如 ../../../etc/passwd)
2221
+ const normalizedWorkspace = path.resolve(workspacePath);
2222
+ const normalizedResolved = path.resolve(resolvedPath);
2223
+ if (!normalizedResolved.startsWith(normalizedWorkspace + path.sep) && normalizedResolved !== normalizedWorkspace) {
2224
+ logger.warn(`[McpServer] calculateFileMd5: 路径逃逸被拒绝: ${filePath}`);
2225
+ return null;
2226
+ }
1794
2227
  return new Promise((resolve) => {
1795
2228
  const hash = createHash('md5');
1796
- const stream = createReadStream(filePath);
2229
+ const stream = createReadStream(resolvedPath);
1797
2230
  stream.on('data', (chunk) => hash.update(chunk));
1798
2231
  stream.on('end', () => resolve(hash.digest('hex')));
1799
2232
  stream.on('error', () => resolve(null));
@@ -1817,28 +2250,22 @@ export class McpServer {
1817
2250
  return `${memoryId}:${sorted.map(([k, v]) => `${k}=${v}`).join('|')}`;
1818
2251
  }
1819
2252
  async handleGetMessage() {
1820
- await this.hydrateUnreadMessagesIfNeeded();
2253
+ // 消息来源标记:
2254
+ // - buffer 在 hydrate 前已有消息 → fromBuffer=true(来自本地 buffer)
2255
+ // - buffer 空、hydrate 从服务端拉到消息 → fromBuffer=false(来自服务端)
2256
+ // - buffer 空、hydrate 也无消息(仅有 Memory 通知注入)→ fromBuffer=true(本地注入)
2257
+ const hadBufferedMessages = this.messageBuffer.size() > 0;
2258
+ await this.hydrateUnreadMessagesFromServer();
1821
2259
  const messages = this.messageBuffer.get();
2260
+ const hasRealMessages = messages.directMessages.length > 0 || messages.groupMessages.length > 0;
2261
+ const fromBuffer = hadBufferedMessages || !hasRealMessages;
1822
2262
  if (await this.prependMemoryNotification(messages, () => this.buildMemoryRecurrenceNotification())) {
1823
- return this.buildPollResult(messages);
1824
- }
1825
- if (await this.prependMemoryNotification(messages, () => this.buildMemoryTodoNotification())) {
1826
- return this.buildPollResult(messages);
2263
+ return this.buildPollResult(messages, fromBuffer);
1827
2264
  }
1828
2265
  if (await this.prependMemoryNotification(messages, () => this.buildMemoryCleanupNotification())) {
1829
- return this.buildPollResult(messages);
2266
+ return this.buildPollResult(messages, fromBuffer);
1830
2267
  }
1831
- return this.buildPollResult(messages);
1832
- }
1833
- /**
1834
- * 在本地缓冲区为空时,从服务端补拉离线期间积累的未读消息。
1835
- * 这样 Agent 重启或重连后,poll_message/get_message 仍能取到持久化未读消息。
1836
- */
1837
- async hydrateUnreadMessagesIfNeeded(options = {}) {
1838
- if (this.messageBuffer.size() > 0) {
1839
- return true;
1840
- }
1841
- return await this.hydrateUnreadMessagesFromServer(options);
2268
+ return this.buildPollResult(messages, fromBuffer);
1842
2269
  }
1843
2270
  /**
1844
2271
  * 从服务端补拉未读消息并写入本地缓冲。
@@ -1867,7 +2294,7 @@ export class McpServer {
1867
2294
  return true;
1868
2295
  }
1869
2296
  catch (error) {
1870
- logger.error("[McpServer] 补拉未读消息失败:", error);
2297
+ logger.error('[McpServer] 补拉未读消息失败: %s, stack=%s', error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : '(no stack)');
1871
2298
  return false;
1872
2299
  }
1873
2300
  }
@@ -1940,6 +2367,12 @@ export class McpServer {
1940
2367
  */
1941
2368
  async handleIncomingMessage(message) {
1942
2369
  if (!this.shouldBufferIncomingMessage(message)) {
2370
+ // pipeline_task_assigned 不进 buffer,但需唤醒 poll 走 hydrate 重查任务,
2371
+ // 否则 poll_message 会被 messageBuffer.poll(30s) 阻塞,task 流转最长延迟 30s。
2372
+ if (!this.isGroupMessage(message) && this.isPipelineTaskAssignedNotification(message)) {
2373
+ logger.info("[McpServer] 收到 pipeline_task_assigned 唤醒信号,立即唤醒 poll 走 hydrate 重查");
2374
+ this.messageBuffer.wakeUpWaiters();
2375
+ }
1943
2376
  return undefined;
1944
2377
  }
1945
2378
  logger.info(`[McpServer] 收到消息: ${message.content?.substring(0, 50) || "(无内容)"}`);
@@ -2009,6 +2442,73 @@ export class McpServer {
2009
2442
  }
2010
2443
  return maybeAgentClient.getState().agentId ?? null;
2011
2444
  }
2445
+ /**
2446
+ * 将 agentId 安全化为文件名片段:仅保留字母数字下划线短横,其余替换为下划线。
2447
+ * 与 acode 包 TodoStore.filePath 的路径安全化逻辑保持一致。
2448
+ */
2449
+ sanitizeAgentIdForTodoFile(agentId) {
2450
+ return String(agentId).replace(/[^a-zA-Z0-9_-]/g, "_");
2451
+ }
2452
+ /**
2453
+ * 构建 Todo 未完成项提醒通知。
2454
+ *
2455
+ * 触发条件(全部满足):
2456
+ * 1. 当前 agentId 可读取
2457
+ * 2. ~/.acode/todos/{agentId}.json 存在且可解析
2458
+ * 3. 存在 status !== "completed" 的未完成项
2459
+ *
2460
+ * 行为:
2461
+ * - 有未完成项:构造轻量系统通知并返回(不持久化)。
2462
+ * - 全部完成 / 无文件 / 异常:静默返回 null,不阻断 poll_message。
2463
+ *
2464
+ * 通知以 DirectMessage 形式注入,senderId="MCP-System",不落库、不持久化。
2465
+ * 提示内容简洁,仅提醒有未完成待办,Agent 如需查看详情应调用 todo_read。
2466
+ */
2467
+ async buildTodoReminderNotification() {
2468
+ try {
2469
+ const agentId = this.getCurrentAgentId();
2470
+ if (!agentId)
2471
+ return null;
2472
+ const todoPath = path.join(os.homedir(), ".acode", "todos", `${this.sanitizeAgentIdForTodoFile(agentId)}.json`);
2473
+ let raw;
2474
+ try {
2475
+ raw = await fs.promises.readFile(todoPath, "utf-8");
2476
+ }
2477
+ catch (error) {
2478
+ // 文件不存在视为无 todo,不通知
2479
+ if (error?.code === "ENOENT")
2480
+ return null;
2481
+ throw error;
2482
+ }
2483
+ const doc = JSON.parse(raw);
2484
+ const todos = Array.isArray(doc.todos)
2485
+ ? doc.todos
2486
+ : [];
2487
+ const incomplete = todos.filter((t) => t &&
2488
+ typeof t === "object" &&
2489
+ typeof t.content === "string" &&
2490
+ (t.status === "pending" ||
2491
+ t.status === "in_progress" ||
2492
+ t.status === "cancelled"));
2493
+ if (incomplete.length === 0) {
2494
+ return null;
2495
+ }
2496
+ return {
2497
+ msgId: `todo-reminder-${Date.now()}`,
2498
+ senderId: "MCP-System",
2499
+ senderName: "系统",
2500
+ receiverId: agentId,
2501
+ content: `Todo 列表存在 ${incomplete.length} 项未完成,请调用 todo_read 查看并处理。`,
2502
+ requireReply: false,
2503
+ prompt: "这是一条 Todo 未完成项提醒,自动生成,无需持久化。请评估是否需要继续处理未完成的 Todo,可调用 todo_read 获取完整列表。",
2504
+ timestamp: new Date().toISOString(),
2505
+ };
2506
+ }
2507
+ catch (error) {
2508
+ logger.warn('[McpServer] 构建 Todo 提醒失败: %s', error instanceof Error ? error.message : String(error));
2509
+ return null;
2510
+ }
2511
+ }
2012
2512
  // ============ 生命周期 ============
2013
2513
  async start() {
2014
2514
  const transport = new StdioServerTransport();
@@ -2024,90 +2524,70 @@ export class McpServer {
2024
2524
  // v1.13.0: 不再加载本地元数据缓存,过期/TODO/回忆状态统一由服务端 memory_status 字段管理
2025
2525
  // 后台注册:不阻塞 MCP 工具暴露,Agent 可立即接收工具调用
2026
2526
  this.autoRegisterInBackground();
2027
- // 启动后台 Memory 文件变更检测
2028
- this.startMemoryFileWatcher();
2029
2527
  logger.info("[McpServer] MCP Server 已就绪,工具已可用");
2030
2528
  }
2031
2529
  /**
2032
- * 停止 McpServer,清理后台定时器。
2033
- * 主流程:清理文件变更检测定时器 -> 停止 Agent 客户端消息循环。
2034
- */
2035
- stop() {
2036
- this.stopMemoryFileWatcher();
2037
- this.agentClient.stop();
2038
- logger.info("[McpServer] MCP Server 已停止");
2039
- }
2040
- /**
2041
- * 启动后台 Memory 文件变更检测定时器。
2042
- * 主流程:读取 AWS_MEMORY_FILE_CHECK_INTERVAL_MS -> 启动 setInterval -> 每次触发时检查绑定文件 MD5 变更 -> 有变更时直接推入消息缓冲区。
2043
- * 后台定时器启用后,poll_message 不再重复检查,避免重复推送通知。
2530
+ * 启动租约续期定时器 (v1.21.0)。
2531
+ * 60 秒续期一次当前 agent 持有的所有 updating memory 的租约,
2532
+ * 防止租约过期导致其他实例接管。
2044
2533
  */
2045
- startMemoryFileWatcher() {
2046
- const intervalMs = this.parsePositiveIntegerEnv("AWS_MEMORY_FILE_CHECK_INTERVAL_MS", DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS);
2047
- if (intervalMs <= 0) {
2048
- logger.info("[McpServer] 后台文件变更检测已禁用(AWS_MEMORY_FILE_CHECK_INTERVAL_MS=0),将回退到 poll_message 时检测");
2534
+ startLeaseRenewal() {
2535
+ if (this.leaseRenewalTimer)
2049
2536
  return;
2050
- }
2051
- this.memoryFileCheckInterval = setInterval(async () => {
2537
+ this.leaseRenewalTimer = setInterval(async () => {
2052
2538
  try {
2053
- const notification = await this.buildMemoryFileChangeNotification();
2054
- if (notification) {
2055
- this.messageBuffer.push(notification);
2056
- logger.info("[McpServer] 后台文件变更检测: 已推送 Memory 变更通知到消息缓冲区");
2539
+ const agentId = this.getCurrentAgentId();
2540
+ if (!agentId)
2541
+ return;
2542
+ const result = await this.memoryStore.renewMemoryLease(McpServer.LEASE_DURATION_MINUTES);
2543
+ // 续期返回 0 说明租约已丢失(被过期清理或被其他 agent 接管),停止续期
2544
+ if (result.renewed === 0) {
2545
+ logger.warn("[McpServer] 租约已丢失(renewed=0),停止续期定时器");
2546
+ this.stopLeaseRenewal();
2547
+ }
2548
+ // L-1: 主动清理 claimedCache 中的过期条目,避免堆积
2549
+ const now = Date.now();
2550
+ for (const [uri, expiry] of this.claimedCache) {
2551
+ if (expiry <= now) {
2552
+ this.claimedCache.delete(uri);
2553
+ }
2057
2554
  }
2058
2555
  }
2059
- catch (error) {
2060
- logger.warn("[McpServer] 后台文件变更检测失败:", error);
2556
+ catch (e) {
2557
+ logger.warn('[McpServer] 续期租约失败: %s', e instanceof Error ? e.message : String(e));
2061
2558
  }
2062
- }, intervalMs);
2063
- logger.info(`[McpServer] 后台文件变更检测已启动,间隔: ${intervalMs}ms(可通过 AWS_MEMORY_FILE_CHECK_INTERVAL_MS 环境变量调整,设为 0 禁用)`);
2559
+ }, McpServer.LEASE_RENEW_INTERVAL_MS);
2560
+ logger.info("[McpServer] 租约续期定时器已启动,间隔: 60s");
2064
2561
  }
2065
2562
  /**
2066
- * 构建短期记忆 TODO 通知。
2067
- * 主流程:调用服务端 list_by_todo_status({ todo_status: 'pending' }) 获取 pending 待办 -> 注入提醒。
2068
- * v1.13.0: 由服务端 memory_status 字段管理 todo 状态,TS 侧不再维护本地缓存。
2563
+ * 停止租约续期定时器 (v1.21.0)。
2069
2564
  */
2070
- async buildMemoryTodoNotification() {
2071
- try {
2072
- const pendingTodos = await this.memoryStore.listByTodoStatus('pending');
2073
- if (pendingTodos.length === 0)
2074
- return null;
2075
- const agentId = this.getCurrentAgentId();
2076
- if (!agentId)
2077
- return null;
2078
- const pendingCount = pendingTodos.length;
2079
- const pendingList = pendingTodos.slice(0, 5).map(m => `- ${m.uri}`).join('\n');
2080
- return {
2081
- msgId: `memory-todo-${Date.now()}`,
2082
- senderId: "MCP-System",
2083
- senderName: "系统",
2084
- receiverId: agentId,
2085
- content: `当前有 ${pendingCount} 个 todo 项未完成,请获取短期记忆(todo 列表),且已经完成请使用 todo_completed 标注为已完成状态。\n\n待办项:\n${pendingList}${pendingCount > 5 ? `\n ...及其他 ${pendingCount - 5} 条` : ''}`,
2086
- requireReply: false,
2087
- prompt: "这是一条短期记忆 TODO 提醒,请调用 list_memories({ memory_type: 'task_list' }) 查看待办列表,完成后通过 todo_completed({ uri }) 标注为已完成状态。",
2088
- timestamp: new Date().toISOString(),
2089
- };
2090
- }
2091
- catch (error) {
2092
- logger.warn("[McpServer] 构建短期记忆 TODO 提醒失败:", error);
2093
- return null;
2565
+ stopLeaseRenewal() {
2566
+ if (this.leaseRenewalTimer) {
2567
+ clearInterval(this.leaseRenewalTimer);
2568
+ this.leaseRenewalTimer = null;
2569
+ logger.info("[McpServer] 租约续期定时器已停止");
2094
2570
  }
2095
2571
  }
2096
2572
  /**
2097
- * 停止后台 Memory 文件变更检测定时器。
2573
+ * 停止 McpServer,清理后台定时器。
2574
+ * 主流程:停止租约续期 -> 释放所有 claim -> 停止 Agent 客户端消息循环。
2098
2575
  */
2099
- stopMemoryFileWatcher() {
2100
- if (this.memoryFileCheckInterval) {
2101
- clearInterval(this.memoryFileCheckInterval);
2102
- this.memoryFileCheckInterval = null;
2103
- logger.info("[McpServer] 后台文件变更检测已停止");
2576
+ stop() {
2577
+ this.stopLeaseRenewal();
2578
+ // 释放所有持有的 claim
2579
+ const agentId = this.getCurrentAgentId();
2580
+ if (agentId) {
2581
+ this.memoryStore.releaseAllMemoryClaims().catch(e => logger.warn('[McpServer] 释放 claim 失败: %s', e instanceof Error ? e.message : String(e)));
2104
2582
  }
2583
+ this.agentClient.stop();
2584
+ logger.info("[McpServer] MCP Server 已停止");
2105
2585
  }
2106
2586
  // ============ 记忆统一状态字段 (v1.13.0) ============
2107
- // v1.13.0 改用服务端 memory_status 字段管理 task_list todo 状态与 recently_memory 过期/回忆状态,
2108
- // 下方 4 个工具直接调用服务端接口,本地不再维护元数据缓存。
2109
- // buildMemoryRecurrenceNotification / buildMemoryTodoNotification / handleMemoryRecurrence / handleMemoryRecurrenceComplete
2110
- // 已在文件其他位置实现(直接使用 this.memoryStore.listExpiredRecentlyMemories / listByTodoStatus / markExpiredMemoriesRecalling / clearRecallingExpiredMemories)。
2587
+ // v1.13.0 改用服务端 memory_status 字段管理 agent_memory 过期/回忆状态,
2588
+ // 下方工具直接调用服务端接口,本地不再维护元数据缓存。
2589
+ // buildMemoryRecurrenceNotification / handleMemoryRecurrence / handleMemoryRecurrenceComplete
2590
+ // 已在文件其他位置实现(直接使用 this.memoryStore.listExpiredRecentlyMemories / markExpiredMemoriesRecalling / clearRecallingExpiredMemories)。
2111
2591
  async autoRegister() {
2112
2592
  const agentConfig = this.agentClient.getAgentConfig();
2113
2593
  logger.info(`[McpServer] 角色: "${agentConfig.roleName}"`);
@@ -2128,11 +2608,10 @@ export class McpServer {
2128
2608
  statusReporter.reportWaitingInput();
2129
2609
  }
2130
2610
  await this.reportCombinedMemoryStats();
2131
- // 如果是新创建的角色,将本地永久记忆同步到服务端
2132
- if (result.isExisting === false) {
2133
- logger.info("[McpServer] 新角色创建,同步本地永久记忆到服务端...");
2134
- await this.syncLocalPermanentMemoryToServer();
2135
- }
2611
+ // 注册成功后检查是否需要从本地恢复永久记忆到 server
2612
+ // 触发条件:server 当前角色 memory 计数为 0(重部署后 DB 为空)
2613
+ // 不依赖 isExisting / profileHandler,确保所有场景都能触发
2614
+ this.maybeSyncPermanentMemoryToServer();
2136
2615
  return;
2137
2616
  }
2138
2617
  catch (error) {
@@ -2171,7 +2650,7 @@ export class McpServer {
2171
2650
  */
2172
2651
  autoRegisterInBackground() {
2173
2652
  this.autoRegister().catch((error) => {
2174
- logger.error("[McpServer] 后台注册异常:", error);
2653
+ logger.error('[McpServer] 后台注册异常: %s, stack=%s', error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : '(no stack)');
2175
2654
  });
2176
2655
  }
2177
2656
  /**
@@ -2179,28 +2658,52 @@ export class McpServer {
2179
2658
  */
2180
2659
  startListeningInternal() {
2181
2660
  logger.info("[McpServer] startListeningInternal 被调用");
2661
+ const profileHandler = (profile) => {
2662
+ // 收到 Profile 信息后,存储到消息缓冲区
2663
+ logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2664
+ this.messageBuffer.setProfileMessage(profile);
2665
+ this.profileSynced = true;
2666
+ this.syncRoleMemoryStore(profile);
2667
+ this.maybeSyncPermanentMemoryToServer();
2668
+ };
2182
2669
  // 降级模式下跳过 WebSocket 连接
2183
2670
  if (this.agentClient.isInDegradedMode()) {
2184
2671
  logger.info("[McpServer] 降级模式:跳过 WebSocket 连接,仅启动本地消息监听");
2185
2672
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2186
2673
  pollMode: false,
2187
- profileHandler: (profile) => {
2188
- // 收到 Profile 信息后,存储到消息缓冲区
2189
- logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2190
- this.messageBuffer.setProfileMessage(profile);
2191
- this.syncRoleMemoryStore(profile);
2192
- },
2674
+ profileHandler,
2193
2675
  });
2676
+ this.activelySyncProfile();
2194
2677
  return;
2195
2678
  }
2196
2679
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2197
2680
  pollMode: false,
2198
- profileHandler: (profile) => {
2199
- // 收到 Profile 信息后,存储到消息缓冲区
2200
- logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2201
- this.messageBuffer.setProfileMessage(profile);
2202
- this.syncRoleMemoryStore(profile);
2203
- },
2681
+ profileHandler,
2682
+ });
2683
+ // 注册时 getProfileByAgentId 会触发 Java markProfileFetched,导致 WebSocket 连接后
2684
+ // Java 不再推送 PROFILE_INFO 消息(AgentWebSocketHandler 检查 hasProfileFetched 跳过)。
2685
+ // 因此 profileHandler 可能永远不会被触发,需要主动获取 profile 同步 hasRoleName。
2686
+ this.activelySyncProfile();
2687
+ }
2688
+ /**
2689
+ * 主动获取 profile 并同步角色存储路径。
2690
+ * 当 Java 侧因 hasProfileFetched=true 跳过 PROFILE_INFO 推送时,
2691
+ * profileHandler 不会被调用,hasRoleName 永远为 false,导致文件变更检测被跳过。
2692
+ * 此方法在 startListeningInternal 后主动拉取 profile,确保 hasRoleName 被正确设置。
2693
+ */
2694
+ activelySyncProfile() {
2695
+ if (this.profileSynced)
2696
+ return;
2697
+ this.agentClient.getProfile().then((profile) => {
2698
+ if (this.profileSynced)
2699
+ return;
2700
+ this.profileSynced = true;
2701
+ logger.info(`[McpServer] 主动获取 profile 成功: displayName=${profile.displayName}, roleName="${profile.roleName ?? '(空)'}"`);
2702
+ this.messageBuffer.setProfileMessage(profile);
2703
+ this.syncRoleMemoryStore(profile);
2704
+ this.maybeSyncPermanentMemoryToServer();
2705
+ }).catch((e) => {
2706
+ logger.warn('[McpServer] 主动获取 profile 失败: %s', e instanceof Error ? e.message : String(e));
2204
2707
  });
2205
2708
  }
2206
2709
  delay(ms) {