aws-runtime-bridge 1.9.40 → 1.9.45

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 (119) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +31 -4
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +382 -57
  4. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
  5. package/dist/adapter/ClaudeSdkAdapter.js +1 -0
  6. package/dist/adapter/OpencodeSdkAdapter.d.ts +19 -0
  7. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  8. package/dist/adapter/OpencodeSdkAdapter.js +62 -0
  9. package/dist/adapter/types.d.ts +21 -6
  10. package/dist/adapter/types.d.ts.map +1 -1
  11. package/dist/adapter/types.js +10 -1
  12. package/dist/index.js +11 -0
  13. package/dist/routes/ai-sources.js +4 -1
  14. package/dist/routes/dashboard.d.ts.map +1 -1
  15. package/dist/routes/dashboard.js +1 -1
  16. package/dist/routes/file-browser.d.ts.map +1 -1
  17. package/dist/routes/file-browser.js +47 -1
  18. package/dist/routes/processes.d.ts.map +1 -1
  19. package/dist/routes/processes.js +7 -0
  20. package/dist/routes/runtime-binding.d.ts.map +1 -1
  21. package/dist/routes/runtime-binding.js +6 -3
  22. package/dist/routes/sessions.js +93 -5
  23. package/dist/routes/sub-agents.js +15 -1
  24. package/dist/routes/terminal.d.ts +15 -0
  25. package/dist/routes/terminal.d.ts.map +1 -1
  26. package/dist/routes/terminal.js +162 -5
  27. package/dist/services/acode-package.d.ts +8 -1
  28. package/dist/services/acode-package.d.ts.map +1 -1
  29. package/dist/services/acode-package.js +33 -4
  30. package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
  31. package/dist/services/aws-client-agent-mcp.js +11 -4
  32. package/dist/services/fileSnapshotStore.js +1 -1
  33. package/dist/services/legacy-path-migration.d.ts +3 -0
  34. package/dist/services/legacy-path-migration.d.ts.map +1 -0
  35. package/dist/services/legacy-path-migration.js +66 -0
  36. package/dist/services/lifecycle-state.d.ts.map +1 -1
  37. package/dist/services/lifecycle-state.js +1 -1
  38. package/dist/services/runtime-binding.d.ts +5 -0
  39. package/dist/services/runtime-binding.d.ts.map +1 -1
  40. package/dist/services/runtime-binding.js +33 -1
  41. package/dist/services/session-output.d.ts +7 -1
  42. package/dist/services/session-output.d.ts.map +1 -1
  43. package/dist/services/session-output.js +16 -2
  44. package/dist/services/workspace-files.d.ts +48 -0
  45. package/dist/services/workspace-files.d.ts.map +1 -1
  46. package/dist/services/workspace-files.js +285 -0
  47. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  48. package/package/acode/dist/built-in-file-tools.js +504 -3
  49. package/package/acode/dist/compaction.d.ts +43 -7
  50. package/package/acode/dist/compaction.d.ts.map +1 -1
  51. package/package/acode/dist/compaction.js +286 -31
  52. package/package/acode/dist/runtime.d.ts +130 -0
  53. package/package/acode/dist/runtime.d.ts.map +1 -1
  54. package/package/acode/dist/runtime.js +972 -71
  55. package/package/acode/dist/sub-agent-manager.d.ts +96 -1
  56. package/package/acode/dist/sub-agent-manager.d.ts.map +1 -1
  57. package/package/acode/dist/sub-agent-manager.js +293 -3
  58. package/package/acode/dist/sub-agent-store.d.ts +8 -1
  59. package/package/acode/dist/sub-agent-store.d.ts.map +1 -1
  60. package/package/acode/dist/sub-agent-store.js +55 -0
  61. package/package/acode/dist/sub-agent-tools.d.ts.map +1 -1
  62. package/package/acode/dist/sub-agent-tools.js +5 -4
  63. package/package/acode/dist/sub-agent-types.d.ts +4 -0
  64. package/package/acode/dist/sub-agent-types.d.ts.map +1 -1
  65. package/package/acode/dist/types.d.ts +19 -0
  66. package/package/acode/dist/types.d.ts.map +1 -1
  67. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +9 -0
  68. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  69. package/package/aws-client-agent-mcp/dist/agent-client.js +69 -10
  70. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  71. package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
  72. package/package/aws-client-agent-mcp/dist/config.js +4 -0
  73. package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
  74. package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
  75. package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
  76. package/package/aws-client-agent-mcp/dist/constants.js +3 -1
  77. package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
  78. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +4 -1
  79. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -1
  80. package/package/aws-client-agent-mcp/dist/context-manager.js +6 -2
  81. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -1
  82. package/package/aws-client-agent-mcp/dist/context-manager.test.js +5 -0
  83. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
  84. package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
  85. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  86. package/package/aws-client-agent-mcp/dist/http-client.js +15 -1
  87. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  88. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +102 -34
  89. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  90. package/package/aws-client-agent-mcp/dist/mcp-server.js +637 -209
  91. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  92. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +151 -51
  93. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  94. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +7 -0
  95. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  96. package/package/aws-client-agent-mcp/dist/mcp-tools.js +18 -0
  97. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  98. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +32 -9
  99. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  100. package/package/aws-client-agent-mcp/dist/memory-store.js +58 -19
  101. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  102. package/package/aws-client-agent-mcp/dist/memory-tools.js +3 -3
  103. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  104. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +21 -1
  105. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/message-buffer.js +47 -21
  107. package/package/aws-client-agent-mcp/dist/message-buffer.js.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +13 -0
  109. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  110. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  111. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  112. package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
  113. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  114. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  115. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +34 -1
  116. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/websocket-client.js +146 -18
  118. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  119. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
11
11
  import { AgentClient } from "./agent-client.js";
12
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";
13
13
  import { logger } from "./logger.js";
14
- import { MCP_TOOLS } from "./mcp-tools.js";
14
+ import { MCP_TOOLS, MEMORY_TOOL_NAMES } from "./mcp-tools.js";
15
15
  import { MessageBuffer } from "./message-buffer.js";
16
16
  import { ContextManager } from "./context-manager.js";
17
17
  import { MemoryCapacityError, MemoryStore, } from "./memory-store.js";
@@ -24,7 +24,6 @@ const DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS = 120_000;
24
24
  * poll_message 永不超时返回空,只在有消息可返回时才退出。
25
25
  */
26
26
  const POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS = 30_000;
27
- const DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS = 5_000;
28
27
  const PATH_UNSAFE_CHARACTERS = new Set([
29
28
  "<",
30
29
  ">",
@@ -49,6 +48,8 @@ export class McpServer {
49
48
  hasRoleName = false;
50
49
  /** 上下文管理器 (v1.8.0) */
51
50
  contextManager;
51
+ /** 子 Agent 模式:仅暴露 Memory 工具,拒绝非 Memory 工具调用 */
52
+ isSubAgent;
52
53
  /**
53
54
  * 内存级去重缓存:记录本轮已通知过的 listable memory 文件变更状态。
54
55
  * key = memoryId,value = 已通知时刻各绑定文件的 MD5 复合哈希。
@@ -56,14 +57,40 @@ export class McpServer {
56
57
  * 进程重启后自动清空(届时重新检测)。
57
58
  */
58
59
  notifiedFileChangeMemoryIds = new Map();
60
+ /**
61
+ * Step1「memory 仍处于 updating 状态」提醒的冷却时间戳表。
62
+ * key = memory URI,value = 上次发送「请继续更新」提醒的时间戳。
63
+ * 用于避免 buildMemoryFileChangeNotification 在 polling 周期内对 sticky 的 updating
64
+ * memory 每轮都裸发 _acodeAutoDelegate(进而导致运行层反复 spawn 相同子任务)。
65
+ * 冷却窗口内不再发出该提醒;窗口到期后若 memory 仍未处理,才会重新委托。
66
+ */
67
+ lastUpdatePendingNotifyAt = new Map();
68
+ /** Step1「仍 updating」提醒的冷却窗口(5 分钟)。冷却期内不重复裸发 _acodeAutoDelegate。 */
69
+ static UPDATE_PENDING_NOTIFY_COOLDOWN_MS = 5 * 60 * 1000;
59
70
  /**
60
71
  * 本地缓存 listable 记忆(recently_memory/agent_memory)的文件 MD5 map。
61
72
  * 服务端(Java 后端)不持久化 file_md5_map 字段,listByType 返回的记录该字段为 undefined。
62
73
  * 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
63
74
  */
64
75
  listableMd5Cache = new Map();
65
- /** 后台 Memory 文件变更检测定时器 (v1.9.0) */
66
- memoryFileCheckInterval = null;
76
+ /**
77
+ * 租约续期定时器 (v1.21.0)。
78
+ * 每 60 秒续期一次当前 agent 持有的所有 updating memory 的租约。
79
+ */
80
+ leaseRenewalTimer = null;
81
+ static LEASE_DURATION_MINUTES = 10;
82
+ static LEASE_RENEW_INTERVAL_MS = 60_000;
83
+ /**
84
+ * claim 去重缓存 (v1.21.0)。
85
+ * 避免 5s 后台定时器重复调 server claim 同一个 memory。
86
+ * key = memory URI, value = 本地缓存的租约到期时间戳。
87
+ * 仅缓存 claim 成功的 URI,在 lease 到期前不再调 server。
88
+ */
89
+ claimedCache = new Map();
90
+ /** syncClaimStatusFromServer 互斥锁,防止后台定时器与 poll 路径并发执行 (v1.21.0) */
91
+ syncClaimMutex = Promise.resolve();
92
+ /** profile 是否已同步(防止 activelySyncProfile 与 profileHandler 双重同步) */
93
+ profileSynced = false;
67
94
  /**
68
95
  * Memory 通知 null 结果缓存:key → expiresAt 时间戳。
69
96
  * 仅缓存 null(无通知)结果,避免 while(true) 每 30s 重复读盘。
@@ -76,12 +103,6 @@ export class McpServer {
76
103
  * 当已有 poll 在进行时,后续调用直接等待同一个 Promise。
77
104
  */
78
105
  pendingPollMessage = null;
79
- /**
80
- * Todo 未完成项提醒去重指纹:记录上次通知时的未完成项内容/状态/优先级快照。
81
- * 指纹一致则不重复通知;todos 变化时再次通知。
82
- * 进程重启后自动清空(届时会重新通知一次,可接受)。
83
- */
84
- lastTodoNotificationFingerprint = null;
85
106
  // 系统提示词由 runtime adapter(ACode/OpenCode/Codex/Claude)在会话创建时注入,
86
107
  // 无需通过 MCP 工具返回。三方 runtime(OpenCode/Claude)将三层提示词合并为一条 system prompt,
87
108
  // ACode 则直接注入为前三条 system role 消息。
@@ -90,7 +111,9 @@ export class McpServer {
90
111
  * 当服务端返回的 roleName 与进程初始化时不一致时,重建 roleMemoryStore 指向正确的目录。
91
112
  */
92
113
  syncRoleMemoryStore(profile) {
114
+ logger.info(`[McpServer] syncRoleMemoryStore 被调用: displayName=${profile.displayName}, roleName="${profile.roleName ?? '(空)'}", id=${profile.id}`);
93
115
  if (!profile.roleName) {
116
+ logger.warn(`[McpServer] syncRoleMemoryStore: profile.roleName 为空,hasRoleName 保持 false。profile=${JSON.stringify({ id: profile.id, displayName: profile.displayName, roleName: profile.roleName, instanceName: profile.instanceName })}`);
94
117
  return;
95
118
  }
96
119
  const expectedRoleDir = McpServer.sanitizeRoleDirectoryName(profile.roleName);
@@ -112,7 +135,7 @@ export class McpServer {
112
135
  */
113
136
  maybeSyncPermanentMemoryToServer() {
114
137
  this.doMaybeSyncPermanentMemoryToServer().catch((e) => {
115
- logger.warn("[McpServer] maybeSyncPermanentMemoryToServer 异常:", e);
138
+ logger.warn('[McpServer] maybeSyncPermanentMemoryToServer 异常: %s, stack=%s', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : '(no stack)');
116
139
  });
117
140
  }
118
141
  async doMaybeSyncPermanentMemoryToServer() {
@@ -285,7 +308,7 @@ export class McpServer {
285
308
  keywords: record.keywords,
286
309
  bound_files: record.bound_files,
287
310
  file_md5_map: record.file_md5_map,
288
- update_status: record.update_status,
311
+ memory_status: record.memory_status,
289
312
  updating_agent_id: record.updating_agent_id,
290
313
  last_check_time: record.last_check_time,
291
314
  };
@@ -300,6 +323,7 @@ export class McpServer {
300
323
  this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
301
324
  this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
302
325
  this.memoryStoresShareFile = false;
326
+ this.isSubAgent = process.env.AWS_SUB_AGENT === "true";
303
327
  this.contextManager = new ContextManager(this.sessionContextMemoryStore);
304
328
  this.server = new Server({
305
329
  name: MCP_SERVER_NAME,
@@ -313,7 +337,10 @@ export class McpServer {
313
337
  }
314
338
  setupHandlers() {
315
339
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
316
- return { tools: MCP_TOOLS };
340
+ const tools = this.isSubAgent
341
+ ? MCP_TOOLS.filter((t) => MEMORY_TOOL_NAMES.has(t.name))
342
+ : MCP_TOOLS;
343
+ return { tools };
317
344
  });
318
345
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
319
346
  const { name, arguments: args } = request.params;
@@ -336,6 +363,8 @@ export class McpServer {
336
363
  ...error.details,
337
364
  }
338
365
  : { error: errorMessage };
366
+ // 服务端记录完整异常,方便排查 LOST 等异常状态
367
+ logger.error('[McpServer] 工具调用异常: tool=%s, error=%s, stack=%s', name, errorMessage, error instanceof Error ? error.stack : '(no stack)');
339
368
  return {
340
369
  content: [
341
370
  {
@@ -352,24 +381,44 @@ export class McpServer {
352
381
  * 处理工具调用
353
382
  */
354
383
  async handleToolCall(name, args) {
384
+ // 子 agent 共享父 agent MCP 连接时注入的标记:
385
+ // 剥离后跳过状态上报,避免子 agent 事件出现在父 agent 时间线上
386
+ const isSubAgentCall = args?._aws_sub_agent === true;
387
+ // 委托等待循环中后续 poll_message 注入的标记:
388
+ // 跳过 reportToolUse/reportToolResult,只保留 reportThinking,
389
+ // 避免"调用MCP↔思考中"状态反复跳动
390
+ const isSilentPoll = args?._aws_silent_poll === true;
391
+ const cleanArgs = { ...args };
392
+ delete cleanArgs._aws_sub_agent;
393
+ delete cleanArgs._aws_silent_poll;
355
394
  // 报告工具调用状态
356
- const statusReporter = this.agentClient.getStatusReporter();
357
- if (statusReporter) {
358
- statusReporter.reportToolUse(name, this.sanitizeToolUseArgs(name, args));
395
+ // - agent 调用:完全跳过(不污染父 agent 时间线)
396
+ // - 静默 poll:跳过 reportToolUse(不报"调用MCP"),但仍保留 statusReporter 供 reportThinking
397
+ const statusReporter = isSubAgentCall
398
+ ? null
399
+ : this.agentClient.getStatusReporter();
400
+ if (statusReporter && !isSilentPoll) {
401
+ statusReporter.reportToolUse(name, this.sanitizeToolUseArgs(name, cleanArgs));
359
402
  }
360
403
  try {
361
- const result = await this.executeToolCall(name, args);
404
+ const result = await this.executeToolCall(name, cleanArgs);
362
405
  // 工具调用完成后,先上报返回数据供时间线日志查看,再切换为思考状态
363
406
  // (Agent 正在处理工具结果、决定下一步动作,而非真正的空闲等待)
364
- if (statusReporter) {
365
- statusReporter.reportToolResult(result);
366
- statusReporter.reportThinking();
407
+ if (statusReporter && !isSilentPoll) {
408
+ const reportableResult = this.stripAutoDelegateForReporting(result);
409
+ if (reportableResult !== null) {
410
+ // 正常结果:上报 toolResult + thinking(工具调用完成,回到思考状态)
411
+ statusReporter.reportToolResult(reportableResult);
412
+ statusReporter.reportThinking();
413
+ }
414
+ // 委托消息(reportableResult === null):跳过 reportToolResult 和 reportThinking
415
+ // ACode runtime 会重新 poll_message,工具调用未真正结束,保持"调用MCP"状态
367
416
  }
368
417
  return result;
369
418
  }
370
419
  catch (error) {
371
420
  // 工具调用失败,报告错误状态
372
- if (statusReporter) {
421
+ if (statusReporter && !isSilentPoll) {
373
422
  statusReporter.reportToolError(error);
374
423
  }
375
424
  throw error;
@@ -379,6 +428,9 @@ export class McpServer {
379
428
  * 执行具体的工具调用
380
429
  */
381
430
  async executeToolCall(name, args) {
431
+ if (this.isSubAgent && !MEMORY_TOOL_NAMES.has(name)) {
432
+ throw new Error(`Sub-agent is not allowed to call tool: ${name}`);
433
+ }
382
434
  switch (name) {
383
435
  case "get_colleague":
384
436
  return await this.agentClient.discoverColleagues();
@@ -597,7 +649,17 @@ export class McpServer {
597
649
  return await this.memoryStore.readMemory(input);
598
650
  }
599
651
  if (input.memory_type) {
600
- return await this.getMemoryStoreForType(input.memory_type).readMemory(input);
652
+ const localStore = this.getMemoryStoreForType(input.memory_type);
653
+ const localRecord = await localStore.readMemory(input);
654
+ if (localRecord) {
655
+ return localRecord;
656
+ }
657
+ // project_memory 跨实例可见性:本地未命中时回退服务端角色级 scope,
658
+ // 使兄弟实例创建的记忆也能被读取(文件变更通知后 agent 需 read_memory)。
659
+ if (input.memory_type === "project_memory") {
660
+ return await this.memoryStore.readMemory(input);
661
+ }
662
+ return null;
601
663
  }
602
664
  const longTermRecord = await this.roleMemoryStore.readMemory({
603
665
  ...input,
@@ -606,6 +668,14 @@ export class McpServer {
606
668
  if (longTermRecord) {
607
669
  return longTermRecord;
608
670
  }
671
+ // 本地角色级未命中,回退服务端角色级 project_memory(兄弟实例创建的记忆)
672
+ const serverLongTerm = await this.memoryStore.readMemory({
673
+ ...input,
674
+ memory_type: "project_memory",
675
+ });
676
+ if (serverLongTerm) {
677
+ return serverLongTerm;
678
+ }
609
679
  return await this.memoryStore.readMemory(input);
610
680
  }
611
681
  /**
@@ -617,10 +687,17 @@ export class McpServer {
617
687
  return { store: this.memoryStore, memory_type: input.memory_type };
618
688
  }
619
689
  if (input.memory_type) {
620
- return {
621
- store: this.getMemoryStoreForType(input.memory_type),
622
- memory_type: input.memory_type,
623
- };
690
+ const localStore = this.getMemoryStoreForType(input.memory_type);
691
+ // project_memory 跨实例:本地有则用本地,否则用服务端角色级 scope
692
+ // (兄弟实例创建的记忆仅在服务端可见,需路由到 server 才能 update/delete)
693
+ if (input.memory_type === "project_memory") {
694
+ const localRecord = await localStore.readMemory(input);
695
+ if (localRecord) {
696
+ return { store: localStore, memory_type: "project_memory" };
697
+ }
698
+ return { store: this.memoryStore, memory_type: "project_memory" };
699
+ }
700
+ return { store: localStore, memory_type: input.memory_type };
624
701
  }
625
702
  const longTermRecord = await this.roleMemoryStore.readMemory({
626
703
  uri: input.uri,
@@ -629,6 +706,14 @@ export class McpServer {
629
706
  if (longTermRecord) {
630
707
  return { store: this.roleMemoryStore, memory_type: "project_memory" };
631
708
  }
709
+ // 回退服务端角色级 project_memory(兄弟实例创建的记忆)
710
+ const serverLongTerm = await this.memoryStore.readMemory({
711
+ uri: input.uri,
712
+ memory_type: "project_memory",
713
+ });
714
+ if (serverLongTerm) {
715
+ return { store: this.memoryStore, memory_type: "project_memory" };
716
+ }
632
717
  return { store: this.memoryStore };
633
718
  }
634
719
  /**
@@ -713,6 +798,43 @@ export class McpServer {
713
798
  }
714
799
  return sanitized;
715
800
  }
801
+ /**
802
+ * 过滤工具返回结果中的 _acodeAutoDelegate 消息,避免自动委托通知
803
+ * (memory 文件变更/过期/容量整理)作为工具返回数据泄露到时间线。
804
+ *
805
+ * 背景:ACode 运行时在 processMemoryAutoDelegates 中拦截这些消息并 spawn
806
+ * 子 agent 处理,主 agent 不会感知。但 MCP server 的 statusReporter 在
807
+ * 拦截之前就已上报原始结果,Java 后端 AgentEventService 会将含 actionResult
808
+ * 的状态事件持久化并通过 AGENT_EVENTS_DELTA 推送到 dashboard 时间线,
809
+ * 导致本应被隐藏的委托通知暴露给用户。
810
+ *
811
+ * 返回 null 表示结果全部为委托消息,应跳过 reportToolResult
812
+ * (由 ACode 运行时在拦截后上报最终结果)。
813
+ */
814
+ stripAutoDelegateForReporting(result) {
815
+ if (!result || typeof result !== "object") {
816
+ return result;
817
+ }
818
+ const pollResult = result;
819
+ const directMessages = Array.isArray(pollResult.directMessages)
820
+ ? pollResult.directMessages
821
+ : [];
822
+ const groupMessages = Array.isArray(pollResult.groupMessages)
823
+ ? pollResult.groupMessages
824
+ : [];
825
+ const isDelegate = (m) => !!m &&
826
+ typeof m === "object" &&
827
+ "_acodeAutoDelegate" in m;
828
+ const hasDelegate = directMessages.some(isDelegate);
829
+ if (!hasDelegate) {
830
+ return result;
831
+ }
832
+ const remainingDm = directMessages.filter((m) => !isDelegate(m));
833
+ if (remainingDm.length === 0 && groupMessages.length === 0) {
834
+ return null;
835
+ }
836
+ return { ...pollResult, directMessages: remainingDm };
837
+ }
716
838
  /**
717
839
  * 创建本地记忆。
718
840
  * 主流程:校验必填字段 -> 交给 MemoryStore 规范化 URI 和类型 -> 返回不含 content 的写入结果。
@@ -771,7 +893,9 @@ export class McpServer {
771
893
  }
772
894
  /**
773
895
  * 更新本地记忆。
774
- * 主流程:要求至少一种变更方式 -> MemoryStore 执行精确替换/追加和版本递增 -> 返回不含 content 的写入结果。
896
+ * 主流程:精确替换/追加/仅状态更新 -> MemoryStore 执行变更和版本递增 -> 返回不含 content 的写入结果。
897
+ * 支持三种写法:1) 精确替换(old_string + new_string);2) 追加(append);3) 无内容更新(仅传 uri,
898
+ * 用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景)。
775
899
  */
776
900
  async handleUpdateMemory(args) {
777
901
  const hasReplace = typeof args.old_string === "string" ||
@@ -782,10 +906,9 @@ export class McpServer {
782
906
  if (hasReplace && !hasCompleteReplace) {
783
907
  throw new Error("old_string and new_string must be provided together");
784
908
  }
785
- if (!hasCompleteReplace &&
786
- (typeof append !== "string" || append.length === 0)) {
787
- throw new Error("Provide either old_string/new_string or append");
788
- }
909
+ // 允许无内容更新:当未提供 old_string/new_string 且未提供有效 append 时,
910
+ // 视为"仅状态更新"——用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景。
911
+ // 此时内容不变,但会重算绑定文件 MD5 并续期(expirable 类型)。
789
912
  const input = {
790
913
  uri: this.getRequiredString(args, "uri"),
791
914
  memory_type: this.getOptionalMemoryType(args),
@@ -827,7 +950,8 @@ export class McpServer {
827
950
  });
828
951
  await this.reportCombinedMemoryStats();
829
952
  // Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
830
- if (target.memory_type === "project_memory") {
953
+ // target.store 已是服务端(兄弟实例创建的记忆回退场景)时跳过,避免重复写
954
+ if (target.memory_type === "project_memory" && target.store !== this.memoryStore) {
831
955
  try {
832
956
  await this.memoryStore.updateMemory({
833
957
  ...input,
@@ -841,9 +965,20 @@ export class McpServer {
841
965
  }
842
966
  }
843
967
  // v1.13.0: agent_memory 过期与回忆状态由服务端 memory_status 字段管理。
844
- // 显式传入的 recall_status / memory_status 已在 updateMemory 内部透传到服务端。
845
- // agent_memory 内容变更会触发 TTL ×2 续期(由 MemoryStore.updateMemory 内部处理),
846
- // recently_memory 续期建议重新 createMemory 替代。
968
+ // v1.21.0: 如果是 project_memory 且之前是 updating 状态,释放 Server claim
969
+ if (target.memory_type === 'project_memory' && existingForMd5?.memory_status === 'updating') {
970
+ try {
971
+ await this.memoryStore.releaseMemoryClaim(input.uri);
972
+ this.claimedCache.delete(input.uri);
973
+ // 所有 claim 已释放,停止续期定时器避免无意义请求
974
+ if (this.claimedCache.size === 0) {
975
+ this.stopLeaseRenewal();
976
+ }
977
+ }
978
+ catch (e) {
979
+ logger.warn('[McpServer] 释放 memory claim 失败: uri=%s, error=%s', input.uri, e instanceof Error ? e.message : String(e));
980
+ }
981
+ }
847
982
  return this.toMemoryWriteResult(updated);
848
983
  }
849
984
  /**
@@ -866,7 +1001,8 @@ export class McpServer {
866
1001
  // v1.13.0: 服务端 deleteMemory 内部已处理相关清理逻辑,
867
1002
  // TS 侧无需任何本地元数据清理。
868
1003
  // Dual-write permanent memory delete to server
869
- if (effectiveType === "project_memory") {
1004
+ // target.store 已是服务端时跳过,避免重复删除
1005
+ if (effectiveType === "project_memory" && target.store !== this.memoryStore) {
870
1006
  try {
871
1007
  await this.memoryStore.deleteMemory({ uri: input.uri, memory_type: "project_memory" });
872
1008
  }
@@ -1214,24 +1350,38 @@ export class McpServer {
1214
1350
  return merged;
1215
1351
  }
1216
1352
  /**
1217
- * 放弃 Memory 更新状态
1353
+ * 放弃 Memory 更新状态 (v1.21.0 改为走 release API)
1218
1354
  * Memory 增强功能 (v1.6.0)
1219
1355
  */
1220
1356
  async handleAbandonMemoryUpdate(args) {
1221
1357
  const uri = this.getRequiredString(args, "uri");
1222
1358
  const memoryType = this.getOptionalMemoryType(args);
1359
+ // 调 Server release API 释放 claim
1360
+ await this.memoryStore.releaseMemoryClaim(uri);
1361
+ // 同时清除本地 roleMemoryStore 的 updating 状态(如果是 project_memory)
1362
+ // 兄弟实例创建的记忆本地不存在时静默跳过,服务端 release 才是权威操作
1223
1363
  const record = await this.memoryStore.readMemory({ uri, memory_type: memoryType });
1224
- if (!record) {
1225
- throw new Error(`Memory not found: ${uri}`);
1364
+ if (record?.memory_type === 'project_memory') {
1365
+ try {
1366
+ await this.roleMemoryStore.updateMemory({
1367
+ uri,
1368
+ memory_type: 'project_memory',
1369
+ });
1370
+ // 不传 memory_status → MemoryStore 默认重置为 normal
1371
+ }
1372
+ catch (e) {
1373
+ logger.info(`[McpServer] abandon_memory_update: 本地无此记忆,跳过本地状态清理: ${uri}`);
1374
+ }
1375
+ }
1376
+ // 从去重缓存移除
1377
+ this.claimedCache.delete(uri);
1378
+ // 所有 claim 已释放,停止续期定时器避免无意义请求
1379
+ if (this.claimedCache.size === 0) {
1380
+ this.stopLeaseRenewal();
1226
1381
  }
1227
- // 清除 updating 状态
1228
- await this.memoryStore.updateMemory({
1229
- uri: record.uri,
1230
- memory_type: record.memory_type,
1231
- });
1232
1382
  return {
1233
1383
  success: true,
1234
- uri: record.uri,
1384
+ uri,
1235
1385
  message: "Memory 更新状态已清除",
1236
1386
  };
1237
1387
  }
@@ -1273,7 +1423,7 @@ export class McpServer {
1273
1423
  await this.memoryStore.markExpiredMemoriesRecalling(batchUris);
1274
1424
  }
1275
1425
  catch (e) {
1276
- logger.warn("[McpServer] 标记过期记忆为 recalling 失败:", e);
1426
+ logger.warn('[McpServer] 标记过期记忆为 recalling 失败: uriCount=%s, error=%s', batchUris.length, e instanceof Error ? e.message : String(e));
1277
1427
  }
1278
1428
  const remaining = Math.max(0, expiredMemories.length - batch.length);
1279
1429
  // 返回过期记忆的摘要(不含完整 content 以减少上下文占用)
@@ -1282,7 +1432,6 @@ export class McpServer {
1282
1432
  title: m.title,
1283
1433
  domain: m.domain,
1284
1434
  path: m.path,
1285
- recall_status: 'recalling',
1286
1435
  memory_status: 'recalling',
1287
1436
  contentPreview: typeof m.content === 'string' && m.content.length > 200 ? m.content.substring(0, 200) + '...' : m.content,
1288
1437
  createdAt: m.createdAt,
@@ -1314,7 +1463,7 @@ export class McpServer {
1314
1463
  result = await this.memoryStore.clearRecallingExpiredMemories();
1315
1464
  }
1316
1465
  catch (e) {
1317
- logger.warn("[McpServer] 清理回忆中记忆失败:", e);
1466
+ logger.warn('[McpServer] 清理回忆中记忆失败: %s', e instanceof Error ? e.message : String(e));
1318
1467
  }
1319
1468
  await this.reportCombinedMemoryStats();
1320
1469
  const deletedCount = result.deleted ?? 0;
@@ -1435,21 +1584,12 @@ export class McpServer {
1435
1584
  return result;
1436
1585
  }
1437
1586
  async handlePollMessage() {
1587
+ logger.info('[McpServer] handlePollMessage 被调用');
1438
1588
  if (this.pendingPollMessage) {
1439
1589
  logger.info('[McpServer] poll_message 已有进行中的调用,等待其完成');
1440
1590
  return await this.pendingPollMessage;
1441
1591
  }
1442
1592
  this.pendingPollMessage = (async () => {
1443
- // 入口处检查 Todo 未完成项提醒:仅在每次 poll_message 调用时检查一次,
1444
- // 不引入后台定时器。状态变化(指纹不同)且有未完成项时立即返回通知;
1445
- // 否则进入原有阻塞等待真实消息的流程。
1446
- const todoNotification = await this.buildTodoReminderNotification();
1447
- if (todoNotification) {
1448
- return this.buildPollResult({
1449
- directMessages: [todoNotification],
1450
- groupMessages: [],
1451
- });
1452
- }
1453
1593
  return await this.doPollMessage();
1454
1594
  })();
1455
1595
  try {
@@ -1459,35 +1599,100 @@ export class McpServer {
1459
1599
  this.pendingPollMessage = null;
1460
1600
  }
1461
1601
  }
1602
+ /**
1603
+ * poll_message 主循环逻辑。
1604
+ *
1605
+ * 紧急/非紧急分类策略:
1606
+ * - 紧急类:未读消息(私信/群聊)、task、todo
1607
+ * - 非紧急类:memory过期回忆、memory文件变更、memory容量整理
1608
+ * - 规则:有紧急类时返回所有命中的紧急项(不搭配非紧急类);
1609
+ * 无紧急类时返回所有命中的非紧急项。
1610
+ *
1611
+ * 返回组合(只可能是以下两种之一):
1612
+ * 1、所有命中的紧急项(未读消息 + todo + task 中命中的全部)
1613
+ * 2、所有命中的非紧急项(memory过期 + 文件变更 + 容量整理 中命中的全部)
1614
+ *
1615
+ * 注:subAgent 终态事件由 ACode 运行时层(runtime.ts callPollMessageWithSubAgentRace)
1616
+ * 在 MCP 调用前后注入,MCP server 内部不直接检测子 agent 状态。
1617
+ * 注:Todo 在入口和 while 循环中均检查,确保阻塞期间创建的 Todo 也能被感知。
1618
+ */
1462
1619
  async doPollMessage() {
1620
+ logger.info('[McpServer] doPollMessage 被调用');
1621
+ // 紧急类检查:hydrate 未读消息 + task
1463
1622
  await this.hydrateUnreadMessagesFromServer({
1464
1623
  blockIfEmpty: false,
1465
- includePipelineTasks: false,
1624
+ includePipelineTasks: true,
1466
1625
  });
1467
- while (true) {
1468
- const buffered = this.messageBuffer.get();
1469
- if (buffered.directMessages.length > 0 || buffered.groupMessages.length > 0) {
1470
- return await this.finalizeWithMemoryNotifications(buffered);
1626
+ // 紧急类检查:Todo(只检查一次)
1627
+ const todoNotif = await this.buildTodoReminderNotification();
1628
+ // 紧急类:有未读消息/task 时,加上 todo(如果有),返回所有紧急项
1629
+ const urgentBuffered = this.messageBuffer.get();
1630
+ if (urgentBuffered.directMessages.length > 0 || urgentBuffered.groupMessages.length > 0) {
1631
+ if (todoNotif) {
1632
+ urgentBuffered.directMessages.unshift(todoNotif);
1471
1633
  }
1634
+ return this.buildPollResult(urgentBuffered);
1635
+ }
1636
+ // 紧急类:无未读消息/task 但有 todo,直接返回 todo
1637
+ if (todoNotif) {
1638
+ return this.buildPollResult({ directMessages: [todoNotif], groupMessages: [] });
1639
+ }
1640
+ // 非紧急类:无紧急类时,检查所有非紧急项(memory过期 + 文件变更 + 容量整理)
1641
+ const nonUrgentMessages = await this.checkAllLocalNonUrgentNotifications();
1642
+ if (nonUrgentMessages.length > 0) {
1643
+ return this.buildPollResult({ directMessages: nonUrgentMessages, groupMessages: [] });
1644
+ }
1645
+ // 无紧急类且无非紧急类:进入 while 循环等待新消息
1646
+ while (true) {
1472
1647
  const polled = await this.messageBuffer.poll(POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS);
1648
+ // 紧急类:poll 到消息(私信/群聊),立即返回
1473
1649
  if (polled.directMessages.length > 0 || polled.groupMessages.length > 0) {
1474
- return await this.finalizeWithMemoryNotifications(polled);
1650
+ return this.buildPollResult(polled);
1651
+ }
1652
+ // poll 超时无消息时,重新检查紧急类(task + todo 可能在此期间到达/创建)
1653
+ // 与入口处逻辑一致:先查 todo,再合并消息/task,紧急项一起返回
1654
+ await this.hydrateUnreadMessagesFromServer({
1655
+ blockIfEmpty: false,
1656
+ includePipelineTasks: true,
1657
+ });
1658
+ const todoNotifLoop = await this.buildTodoReminderNotification();
1659
+ const taskBuffered = this.messageBuffer.get();
1660
+ if (taskBuffered.directMessages.length > 0 || taskBuffered.groupMessages.length > 0) {
1661
+ if (todoNotifLoop) {
1662
+ taskBuffered.directMessages.unshift(todoNotifLoop);
1663
+ }
1664
+ return this.buildPollResult(taskBuffered);
1665
+ }
1666
+ // 紧急类:无消息/task 但有 todo,返回 todo
1667
+ if (todoNotifLoop) {
1668
+ return this.buildPollResult({ directMessages: [todoNotifLoop], groupMessages: [] });
1669
+ }
1670
+ // 非紧急类:无紧急类时,检查所有非紧急项
1671
+ const nonUrgent = await this.checkAllLocalNonUrgentNotifications();
1672
+ if (nonUrgent.length > 0) {
1673
+ return this.buildPollResult({ directMessages: nonUrgent, groupMessages: [] });
1475
1674
  }
1476
1675
  }
1477
1676
  }
1478
1677
  /**
1479
- * 后检 Memory 通知并注入到待返回的消息中。按优先级从高到低注入:
1480
- * 文件变更(始终注入)→ 近期记忆过期回忆(触发即返回)→ TODO(触发即返回)→ 容量整理(始终注入)
1481
- */
1482
- async finalizeWithMemoryNotifications(messages) {
1483
- if (!this.memoryFileCheckInterval) {
1484
- await this.prependMemoryNotification(messages, () => this.buildMemoryFileChangeNotification());
1485
- }
1486
- if (await this.prependMemoryNotification(messages, () => this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification()))) {
1487
- return this.buildPollResult(messages);
1488
- }
1489
- await this.prependMemoryNotification(messages, () => this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification()));
1490
- return this.buildPollResult(messages);
1678
+ * 检查本地非紧急通知(memory过期回忆、文件变更、容量整理),返回所有命中的通知。
1679
+ * Task 不在此方法中检查,因为 Task 通过服务端 hydrate 以消息形式返回。
1680
+ */
1681
+ async checkAllLocalNonUrgentNotifications() {
1682
+ const messages = [];
1683
+ // Memory 过期回忆
1684
+ const recurrenceNotif = await this.getCachedMemoryNotification('recurrence', () => this.buildMemoryRecurrenceNotification());
1685
+ if (recurrenceNotif)
1686
+ messages.push(recurrenceNotif);
1687
+ // Memory 文件变更(需要更新)
1688
+ const fileChangeNotif = await this.buildMemoryFileChangeNotification();
1689
+ if (fileChangeNotif)
1690
+ messages.push(fileChangeNotif);
1691
+ // Memory 容量整理
1692
+ const cleanupNotif = await this.getCachedMemoryNotification('cleanup', () => this.buildMemoryCleanupNotification());
1693
+ if (cleanupNotif)
1694
+ messages.push(cleanupNotif);
1695
+ return messages;
1491
1696
  }
1492
1697
  buildPollResult(messages, fromBuffer = true) {
1493
1698
  return {
@@ -1556,10 +1761,14 @@ export class McpServer {
1556
1761
  requireReply: false,
1557
1762
  prompt: "这是一条本地 memory 容量整理提醒,自动生成,无需持久化。请优先整理已超过建议上限的短期/近期 memory 类型,或具体类型下的 memory 条目。",
1558
1763
  timestamp: new Date().toISOString(),
1764
+ _acodeAutoDelegate: {
1765
+ type: 'memory_cleanup',
1766
+ uris: exceededDomains.map(d => `${d.memoryType}/${d.domain}`),
1767
+ },
1559
1768
  };
1560
1769
  }
1561
1770
  catch (error) {
1562
- logger.warn("[McpServer] 构建本地 memory 整理提醒失败:", error);
1771
+ logger.warn('[McpServer] 构建本地 memory 整理提醒失败: %s', error instanceof Error ? error.message : String(error));
1563
1772
  return null;
1564
1773
  }
1565
1774
  }
@@ -1583,7 +1792,7 @@ export class McpServer {
1583
1792
  const agentId = this.getCurrentAgentId();
1584
1793
  if (!agentId)
1585
1794
  return null;
1586
- const recallingExpired = expiredMemories.filter(m => m.recall_status === 'recalling' || m.memory_status === 'recalling');
1795
+ const recallingExpired = expiredMemories.filter(m => m.memory_status === 'recalling');
1587
1796
  const normalExpired = expiredMemories.filter(m => !recallingExpired.includes(m));
1588
1797
  // 场景1: 有 recalling 状态的过期记忆(Agent 未完成疏理)
1589
1798
  if (recallingExpired.length > 0) {
@@ -1597,6 +1806,10 @@ export class McpServer {
1597
1806
  requireReply: false,
1598
1807
  prompt: "memory_recurrence 取回的记忆仍在回忆中,请通过 memory_recurrence_complete 完成疏理,或继续归档/续期。",
1599
1808
  timestamp: new Date().toISOString(),
1809
+ _acodeAutoDelegate: {
1810
+ type: 'memory_expiration',
1811
+ uris: recallingExpired.map(m => m.uri),
1812
+ },
1600
1813
  };
1601
1814
  }
1602
1815
  // 场景2: 有待回忆的过期记忆
@@ -1611,12 +1824,16 @@ export class McpServer {
1611
1824
  requireReply: false,
1612
1825
  prompt: `这是一条近期记忆过期提醒,共有 ${normalExpired.length} 条记忆即将遗忘,请调用 memory_recurrence 分批次获取并决定处理方式。`,
1613
1826
  timestamp: new Date().toISOString(),
1827
+ _acodeAutoDelegate: {
1828
+ type: 'memory_expiration',
1829
+ uris: normalExpired.map(m => m.uri),
1830
+ },
1614
1831
  };
1615
1832
  }
1616
1833
  return null;
1617
1834
  }
1618
1835
  catch (error) {
1619
- logger.warn("[McpServer] 构建近期记忆过期回忆通知失败:", error);
1836
+ logger.warn('[McpServer] 构建近期记忆过期回忆通知失败: %s', error instanceof Error ? error.message : String(error));
1620
1837
  return null;
1621
1838
  }
1622
1839
  }
@@ -1625,24 +1842,38 @@ export class McpServer {
1625
1842
  * Memory 增强功能 (v1.6.0)
1626
1843
  *
1627
1844
  * 检测流程:
1845
+ * 0. 同步 Server 的 claim 状态到本地,清理过期 claim
1628
1846
  * 1. 检查本 Agent 标记为 updating 但未处理的 Memory
1629
1847
  * 2. 检查短期记忆、近期记忆绑定的文件是否变更
1630
- * 3. 如果没有变更,检查永久记忆的文件变更,标记最多 5 个为 updating
1848
+ * 3. 检查永久记忆的文件变更,先走 Server 原子 claim,再标记本地
1631
1849
  */
1632
1850
  async buildMemoryFileChangeNotification() {
1851
+ logger.info(`[McpServer] buildMemoryFileChangeNotification 被调用: hasRoleName=${this.hasRoleName}, agentId=${this.getCurrentAgentId() ?? '(空)'}`);
1633
1852
  // 无角色名时不检测本地永久记忆文件变更(避免 default-role 跨角色污染)
1634
- if (!this.hasRoleName)
1853
+ if (!this.hasRoleName) {
1854
+ logger.info('[McpServer] 文件变更检测跳过: hasRoleName=false');
1635
1855
  return null;
1856
+ }
1636
1857
  try {
1637
1858
  const agentId = this.getCurrentAgentId();
1638
1859
  if (!agentId) {
1860
+ logger.info('[McpServer] 文件变更检测跳过: agentId 为空');
1639
1861
  return null;
1640
1862
  }
1641
- // 步骤1: 检查是否有 pendingupdating memories(角色本地永久记忆),
1863
+ // Step 0: 同步 Serverclaim 状态到本地 roleMemoryStore(可选,失败降级)。
1864
+ // 清理那些 Server 上已释放(租约过期或被清理)但本地仍标记为 updating 的记录。
1865
+ // Server API 不可用时降级,依赖本地状态,不影响检测流程。
1866
+ await this.syncClaimStatusFromServer(agentId).catch(e => logger.info("[McpServer] Step 0 同步 claim 状态失败(降级):", e));
1867
+ // 步骤1: 检查是否有 pending 的 updating memories(服务端角色级永久记忆),
1642
1868
  // 同时检测绑定文件是否再次变更(用户可能在 Agent 处理前又改了文件)
1643
- const permanentList = await this.roleMemoryStore.listByType('project_memory');
1644
- const updatingMemories = permanentList.filter(m => m.update_status === 'updating' && m.updating_agent_id === agentId);
1869
+ // 数据源切换:从服务端角色级 scope 读取(同角色所有实例共享),
1870
+ // 不再用本地 roleMemoryStore(仅本实例可见,兄弟实例无法检测到彼此创建的 memory)。
1871
+ const permanentList = await this.memoryStore.listByType('project_memory');
1872
+ const updatingMemories = permanentList.filter(m => m.memory_status === 'updating' && m.updating_agent_id === agentId);
1645
1873
  if (updatingMemories.length > 0) {
1874
+ // 当前 agent 持有未完成的 updating memory,确保租约续期定时器在运行,
1875
+ // 避免 agent 在更新过程中租约过期被其他实例接管。
1876
+ this.startLeaseRenewal();
1646
1877
  // 对 updating 记忆也检测文件是否再次变更
1647
1878
  const updatingChanges = await this.checkMemoriesFileChanges(updatingMemories);
1648
1879
  if (updatingChanges.length > 0) {
@@ -1650,10 +1881,11 @@ export class McpServer {
1650
1881
  for (const change of updatingChanges) {
1651
1882
  const memory = updatingMemories.find(m => m.id === change.memoryId);
1652
1883
  if (memory) {
1653
- await this.roleMemoryStore.updateMemory({
1884
+ // MD5 回写到服务端角色级 scope(显式传 memory_status:updating 防止 auto-release)
1885
+ await this.memoryStore.updateMemory({
1654
1886
  uri: memory.uri,
1655
1887
  memory_type: 'project_memory',
1656
- update_status: 'updating',
1888
+ memory_status: 'updating',
1657
1889
  updating_agent_id: agentId,
1658
1890
  file_md5_map: memory.file_md5_map,
1659
1891
  last_check_time: memory.last_check_time,
@@ -1674,12 +1906,37 @@ export class McpServer {
1674
1906
  requireReply: false,
1675
1907
  prompt: "这是一条文件变更提醒,您之前标记为更新中的记忆绑定的文件又有新变更,请处理。",
1676
1908
  timestamp: new Date().toISOString(),
1909
+ _acodeAutoDelegate: {
1910
+ type: 'memory_file_change',
1911
+ uris: updatingChanges.slice(0, 5).map(c => c.memoryUri),
1912
+ },
1677
1913
  };
1678
1914
  }
1679
- const memoryList = updatingMemories
1915
+ // Step1「仍 updating」提醒去重:sticky 的 updating memory 若不在冷却窗口内才发提醒,
1916
+ // 否则本轮直接返回 null,避免每轮 poll 都裸发 _acodeAutoDelegate 触发运行层反复 spawn 子任务。
1917
+ const now = Date.now();
1918
+ const pendingToNotify = updatingMemories.filter(m => {
1919
+ const last = this.lastUpdatePendingNotifyAt.get(m.uri) ?? 0;
1920
+ return now - last >= McpServer.UPDATE_PENDING_NOTIFY_COOLDOWN_MS;
1921
+ });
1922
+ // 清理已不在 updating 列表中的过期冷却记录
1923
+ for (const uri of this.lastUpdatePendingNotifyAt.keys()) {
1924
+ if (!updatingMemories.some(m => m.uri === uri)) {
1925
+ this.lastUpdatePendingNotifyAt.delete(uri);
1926
+ }
1927
+ }
1928
+ if (pendingToNotify.length === 0) {
1929
+ logger.info(`[McpServer] 文件变更检测 Step1: ${updatingMemories.length} 个 updating memory 均处于冷却窗口内,跳过提醒`);
1930
+ return null;
1931
+ }
1932
+ for (const m of pendingToNotify) {
1933
+ this.lastUpdatePendingNotifyAt.set(m.uri, now);
1934
+ }
1935
+ const memoryList = pendingToNotify
1680
1936
  .slice(0, 5)
1681
1937
  .map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
1682
1938
  .join('\n');
1939
+ logger.info(`[McpServer] 文件变更检测 Step1: ${pendingToNotify.length}/${updatingMemories.length} 个 updating memory 待更新(其余冷却中),租约已续期`);
1683
1940
  return {
1684
1941
  msgId: `memory-update-pending-${Date.now()}`,
1685
1942
  senderId: "MCP-System",
@@ -1689,14 +1946,22 @@ export class McpServer {
1689
1946
  requireReply: false,
1690
1947
  prompt: "这是一条更新提醒,您之前已标记这些记忆为更新中,请继续处理。",
1691
1948
  timestamp: new Date().toISOString(),
1949
+ _acodeAutoDelegate: {
1950
+ type: 'memory_file_change',
1951
+ uris: pendingToNotify.slice(0, 5).map(m => m.uri),
1952
+ },
1692
1953
  };
1693
1954
  }
1694
1955
  // 步骤2: 检查短期记忆和近期记忆(服务端存储)
1695
1956
  const recentlyMemoryMemories = await this.memoryStore.listByType('recently_memory');
1696
1957
  const agentMemoryMemories = await this.memoryStore.listByType('agent_memory');
1697
1958
  const listableMemories = [...recentlyMemoryMemories, ...agentMemoryMemories];
1959
+ // 诊断日志:列出 listable memory 的绑定文件和 MD5 基线情况
1960
+ const listableWithFiles = listableMemories.filter(m => m.bound_files && m.bound_files.length > 0);
1961
+ logger.info(`[McpServer] 文件变更检测 Step2: recently=${recentlyMemoryMemories.length}, agent=${agentMemoryMemories.length}, ` +
1962
+ `有绑定文件=${listableWithFiles.length}/${listableMemories.length}。` +
1963
+ `详情: ${listableMemories.map(m => `${m.uri}(bound=${m.bound_files?.length ?? 0},md5=${Object.keys(m.file_md5_map ?? {}).length})`).join('; ') || '无 memory'}`);
1698
1964
  // 用本地 MD5 缓存补全 listable 记忆(服务端可能不持久化 file_md5_map 字段)
1699
- // 确保 checkMemoriesFileChanges 有正确的旧 MD5 做比对而非始终以空字典开始
1700
1965
  for (const m of listableMemories) {
1701
1966
  if (!m.file_md5_map) {
1702
1967
  const cached = this.listableMd5Cache.get(m.id);
@@ -1706,17 +1971,20 @@ export class McpServer {
1706
1971
  }
1707
1972
  }
1708
1973
  const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
1709
- // 使用复合 key(memoryId + 当前各绑定文件 MD5)过滤已通知变更,
1710
- // 同一条记忆文件再次变更(MD5 变化)时仍能触发新通知
1711
1974
  const newListableChanges = listableChanges.filter(c => {
1712
1975
  const memory = listableMemories.find(m => m.id === c.memoryId);
1713
1976
  const md5Map = memory?.file_md5_map;
1714
1977
  const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
1715
- return this.notifiedFileChangeMemoryIds.get(c.memoryId) !== changeKey;
1978
+ const lastNotified = this.notifiedFileChangeMemoryIds.get(c.memoryId);
1979
+ const isNew = lastNotified !== changeKey;
1980
+ if (!isNew) {
1981
+ logger.info(`[McpServer] 文件变更检测 Step2: ${c.memoryUri} 变更被去重吞掉 (changeKey=${changeKey}, lastNotified=${lastNotified})`);
1982
+ }
1983
+ return isNew;
1716
1984
  });
1985
+ logger.info(`[McpServer] 文件变更检测 Step2: 检测到变更=${listableChanges.length}, 去重后=${newListableChanges.length}` +
1986
+ (listableChanges.length > 0 ? ` 变更项: ${listableChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
1717
1987
  if (newListableChanges.length > 0) {
1718
- // 记录当前状态的复合键,下次对比时若 MD5 变化则重新通知
1719
- // 同时将 MD5 map 写入本地缓存,弥补服务端不持久化 file_md5_map 的问题
1720
1988
  for (const c of newListableChanges) {
1721
1989
  const memory = listableMemories.find(m => m.id === c.memoryId);
1722
1990
  const md5Map = memory?.file_md5_map;
@@ -1737,52 +2005,164 @@ export class McpServer {
1737
2005
  requireReply: false,
1738
2006
  prompt: "这是一条文件变更提醒,自动生成,无需持久化。",
1739
2007
  timestamp: new Date().toISOString(),
2008
+ _acodeAutoDelegate: {
2009
+ type: 'memory_file_change',
2010
+ uris: newListableChanges.map(c => c.memoryUri),
2011
+ },
1740
2012
  };
1741
2013
  }
1742
- // 步骤3: 检查永久记忆(角色本地存储)
1743
- const normalPermanentMemories = permanentList.filter(m => m.bound_files && m.bound_files.length > 0 &&
1744
- (m.update_status === 'normal' || !m.update_status));
1745
- const permanentChanges = await this.checkMemoriesFileChanges(normalPermanentMemories);
2014
+ // 步骤3: 检查永久记忆(角色本地存储)— 优先走 Server 原子 claim,失败时降级到本地直接标记
2015
+ const permanentMemoriesWithFiles = permanentList.filter(m => m.bound_files && m.bound_files.length > 0
2016
+ && (m.memory_status === 'normal' || !m.memory_status)
2017
+ && !this.isClaimCached(m.uri));
2018
+ const permanentTotalWithFiles = permanentList.filter(m => m.bound_files && m.bound_files.length > 0);
2019
+ logger.info(`[McpServer] 文件变更检测 Step3: project_memory 总数=${permanentList.length}, ` +
2020
+ `有绑定文件=${permanentTotalWithFiles.length}, 待检测(过滤updating/claimed后)=${permanentMemoriesWithFiles.length}。` +
2021
+ `被过滤原因: ${permanentTotalWithFiles.length - permanentMemoriesWithFiles.length} 项(memory_status=updating 或 isClaimCached)`);
2022
+ const permanentChanges = await this.checkMemoriesFileChanges(permanentMemoriesWithFiles);
2023
+ // 回写无基线 memory 的新 MD5 到服务端(修复前创建的旧数据补基线)
2024
+ const baselineUpdatedMemories = permanentMemoriesWithFiles.filter(m => m.file_md5_map && Object.keys(m.file_md5_map).length > 0
2025
+ && !permanentChanges.some(c => c.memoryId === m.id));
2026
+ if (baselineUpdatedMemories.length > 0) {
2027
+ logger.info(`[McpServer] 文件变更检测 Step3: 回写 ${baselineUpdatedMemories.length} 个无基线 memory 的 MD5 基线到服务端`);
2028
+ for (const m of baselineUpdatedMemories) {
2029
+ try {
2030
+ await this.memoryStore.updateMemory({
2031
+ uri: m.uri,
2032
+ memory_type: 'project_memory',
2033
+ file_md5_map: m.file_md5_map,
2034
+ last_check_time: m.last_check_time,
2035
+ });
2036
+ }
2037
+ catch (e) {
2038
+ logger.info(`[McpServer] 回写基线 MD5 失败: ${m.uri}`, e);
2039
+ }
2040
+ }
2041
+ }
2042
+ logger.info(`[McpServer] 文件变更检测 Step3: 检测到变更=${permanentChanges.length}` +
2043
+ (permanentChanges.length > 0 ? ` 变更项: ${permanentChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
1746
2044
  if (permanentChanges.length > 0) {
1747
2045
  // 最多选取 5 个
1748
2046
  const selectedChanges = permanentChanges.slice(0, 5);
1749
- // 标记为 updating 状态,同时持久化当前 MD5 map,
1750
- // 确保后续检测能正确对比文件变更,而非每次都视为"已变更"
1751
- for (const change of selectedChanges) {
1752
- const memory = permanentList.find(m => m.id === change.memoryId);
1753
- if (memory) {
1754
- await this.roleMemoryStore.updateMemory({
1755
- uri: memory.uri,
1756
- memory_type: 'project_memory',
1757
- update_status: 'updating',
1758
- updating_agent_id: agentId,
1759
- // checkMemoriesFileChanges 已更新 in-memory 对象中的 MD5 map
1760
- file_md5_map: memory.file_md5_map,
1761
- last_check_time: memory.last_check_time,
1762
- });
2047
+ const urisToClaim = selectedChanges.map(c => c.memoryUri);
2048
+ let claimedViaServer = false;
2049
+ let claimedUris = new Set();
2050
+ try {
2051
+ // Path A: 尝试走 Server 原子 claim
2052
+ const claimResult = await this.memoryStore.claimMemoryUpdate(urisToClaim, McpServer.LEASE_DURATION_MINUTES, 'project_memory');
2053
+ claimedUris = new Set(claimResult.claimed_uris ?? []);
2054
+ claimedViaServer = true;
2055
+ }
2056
+ catch (e) {
2057
+ // Path B: Server API 不可用 → 降级到本地直接标记
2058
+ logger.info("[McpServer] Server claim API 不可用,降级到本地直接标记:", e);
2059
+ claimedUris = new Set(urisToClaim);
2060
+ }
2061
+ if (claimedUris.size > 0) {
2062
+ const claimedChanges = selectedChanges.filter(c => claimedUris.has(c.memoryUri));
2063
+ // 标记本地 roleMemoryStore
2064
+ for (const change of claimedChanges) {
2065
+ const memory = permanentList.find(m => m.id === change.memoryId);
2066
+ if (memory) {
2067
+ // claim 状态由 batchClaimUpdate 在服务端原子写入;此处仅回写新 MD5 基线到服务端角色级 scope,
2068
+ // 显式传 memory_status:updating/updating_agent_id 防止 Java updateMemory auto-release 误释放 claim。
2069
+ await this.memoryStore.updateMemory({
2070
+ uri: memory.uri,
2071
+ memory_type: 'project_memory',
2072
+ memory_status: 'updating',
2073
+ updating_agent_id: agentId,
2074
+ file_md5_map: memory.file_md5_map,
2075
+ last_check_time: memory.last_check_time,
2076
+ });
2077
+ // 加入去重缓存(lease 有效期内不再重复 claim)
2078
+ this.claimedCache.set(memory.uri, Date.now() + McpServer.LEASE_DURATION_MINUTES * 60 * 1000);
2079
+ }
2080
+ }
2081
+ // 启动租约续期(仅 Server claim 成功时需要,降级模式不续期)
2082
+ if (claimedViaServer && claimedUris.size > 0) {
2083
+ this.startLeaseRenewal();
1763
2084
  }
2085
+ const changeDetails = claimedChanges
2086
+ .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
2087
+ .join('\n');
2088
+ return {
2089
+ msgId: `memory-file-change-${Date.now()}`,
2090
+ senderId: "MCP-System",
2091
+ senderName: "系统",
2092
+ receiverId: agentId,
2093
+ content: `检测到以下 ${claimedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
2094
+ requireReply: false,
2095
+ prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
2096
+ timestamp: new Date().toISOString(),
2097
+ _acodeAutoDelegate: {
2098
+ type: 'memory_file_change',
2099
+ uris: claimedChanges.map(c => c.memoryUri),
2100
+ },
2101
+ };
1764
2102
  }
1765
- const changeDetails = selectedChanges
1766
- .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
1767
- .join('\n');
1768
- return {
1769
- msgId: `memory-file-change-${Date.now()}`,
1770
- senderId: "MCP-System",
1771
- senderName: "系统",
1772
- receiverId: agentId,
1773
- content: `检测到以下 ${selectedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
1774
- requireReply: false,
1775
- prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
1776
- timestamp: new Date().toISOString(),
1777
- };
1778
2103
  }
2104
+ logger.info('[McpServer] 文件变更检测完成: 无变更通知产生');
1779
2105
  return null;
1780
2106
  }
1781
2107
  catch (error) {
1782
- logger.warn("[McpServer] 构建 Memory 文件变更检测通知失败:", error);
2108
+ logger.warn('[McpServer] 构建 Memory 文件变更检测通知失败: %s', error instanceof Error ? error.message : String(error));
1783
2109
  return null;
1784
2110
  }
1785
2111
  }
2112
+ /**
2113
+ * Step 0: 同步 Server 的 claim 状态到本地 roleMemoryStore (v1.21.0)。
2114
+ * 查询 Server 上当前 agent 持有的 updating 记忆 URI 列表,
2115
+ * 本地中标记为 updating 但 Server 不持有的 → 重置为 normal。
2116
+ * 处理场景:agent crash 后重启,Server 租约已过期被清理,但本地 JSON 仍卡在 updating。
2117
+ */
2118
+ async syncClaimStatusFromServer(agentId) {
2119
+ // 互斥锁:防止后台定时器与 poll 路径并发执行导致 TOCTOU
2120
+ const previous = this.syncClaimMutex;
2121
+ let release;
2122
+ this.syncClaimMutex = new Promise((resolve) => { release = resolve; });
2123
+ await previous;
2124
+ try {
2125
+ // 从 Server 查询当前 agent 持有的 claimed URIs
2126
+ const serverClaimedUris = await this.memoryStore.getMyClaimedUris();
2127
+ const serverClaimedSet = new Set(serverClaimedUris);
2128
+ // 读取本地 roleMemoryStore 中所有 project_memory
2129
+ const localMemories = await this.roleMemoryStore.listByType('project_memory');
2130
+ const staleLocalClaims = localMemories.filter(m => m.memory_status === 'updating'
2131
+ && m.updating_agent_id === agentId
2132
+ && !serverClaimedSet.has(m.uri));
2133
+ if (staleLocalClaims.length > 0) {
2134
+ logger.info(`[McpServer] Step 0: 清理 ${staleLocalClaims.length} 条本地过期 claim(Server 已释放)`);
2135
+ for (const m of staleLocalClaims) {
2136
+ await this.roleMemoryStore.updateMemory({
2137
+ uri: m.uri,
2138
+ memory_type: 'project_memory',
2139
+ });
2140
+ // 不传 memory_status → MemoryStore 默认重置为 normal
2141
+ }
2142
+ }
2143
+ }
2144
+ catch (e) {
2145
+ // Server 不可用时降级,依赖本地状态
2146
+ logger.warn('[McpServer] Step 0: 同步 claim 状态失败(降级): %s', e instanceof Error ? e.message : String(e));
2147
+ }
2148
+ finally {
2149
+ release();
2150
+ }
2151
+ }
2152
+ /**
2153
+ * 检查指定 URI 是否在 claim 去重缓存内 (v1.21.0)。
2154
+ * 在 lease 有效期内不再重复 claim。
2155
+ */
2156
+ isClaimCached(uri) {
2157
+ const expiry = this.claimedCache.get(uri);
2158
+ if (expiry === undefined)
2159
+ return false;
2160
+ if (expiry <= Date.now()) {
2161
+ this.claimedCache.delete(uri);
2162
+ return false;
2163
+ }
2164
+ return true;
2165
+ }
1786
2166
  /**
1787
2167
  * 检查一组 Memory 的文件变更
1788
2168
  * Memory 增强功能 (v1.6.0)
@@ -1797,25 +2177,47 @@ export class McpServer {
1797
2177
  const changedFiles = [];
1798
2178
  const newMd5Map = {};
1799
2179
  const oldMd5Map = memory.file_md5_map || {};
2180
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
2181
+ const hasNoBaseline = Object.keys(oldMd5Map).length === 0;
1800
2182
  for (const filePath of memory.bound_files) {
1801
2183
  try {
1802
2184
  const currentMd5 = await this.calculateFileMd5(filePath);
1803
2185
  if (currentMd5 === null) {
1804
2186
  // 文件不存在或无法读取
1805
- changedFiles.push(`${filePath} (文件不存在或无法访问)`);
2187
+ const resolvedPath = path.resolve(workspacePath, filePath);
2188
+ logger.info(`[McpServer] MD5 计算返回 null: ${filePath} -> ${resolvedPath} (workspace=${workspacePath})`);
2189
+ if (!hasNoBaseline) {
2190
+ // 已有基线但文件读不到 → 视为变更(文件被删除/移动)
2191
+ changedFiles.push(`${filePath} (文件不存在或无法访问)`);
2192
+ }
1806
2193
  continue;
1807
2194
  }
1808
2195
  newMd5Map[filePath] = currentMd5;
1809
2196
  const oldMd5 = oldMd5Map[filePath];
1810
2197
  if (!oldMd5 || oldMd5 !== currentMd5) {
1811
- changedFiles.push(filePath);
2198
+ if (hasNoBaseline) {
2199
+ // 无基线(修复前创建的旧数据或新建未计算 MD5):静默建立基线,不视为变更
2200
+ logger.info(`[McpServer] 建立基线: ${filePath} new=${currentMd5} (workspace=${workspacePath}, memory=${memory.uri})`);
2201
+ }
2202
+ else {
2203
+ logger.info(`[McpServer] 文件变更: ${filePath} old=${oldMd5} new=${currentMd5} (workspace=${workspacePath})`);
2204
+ changedFiles.push(filePath);
2205
+ }
1812
2206
  }
1813
2207
  }
1814
2208
  catch (error) {
1815
2209
  logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
1816
- changedFiles.push(`${filePath} (MD5计算失败)`);
2210
+ if (!hasNoBaseline) {
2211
+ changedFiles.push(`${filePath} (MD5计算失败)`);
2212
+ }
1817
2213
  }
1818
2214
  }
2215
+ // 无基线时:更新 MD5 map 到 memory 对象(供调用方回写),但不计入变更
2216
+ if (hasNoBaseline && Object.keys(newMd5Map).length > 0) {
2217
+ memory.file_md5_map = newMd5Map;
2218
+ memory.last_check_time = new Date().toISOString();
2219
+ continue; // 不 push 到 changes
2220
+ }
1819
2221
  if (changedFiles.length > 0) {
1820
2222
  changes.push({
1821
2223
  memoryId: memory.id,
@@ -1841,9 +2243,20 @@ export class McpServer {
1841
2243
  try {
1842
2244
  const { createReadStream } = await import('node:fs');
1843
2245
  const { createHash } = await import('node:crypto');
2246
+ // 绑定文件以工作区相对路径存储(./xxx),需相对 AWS_WORKSPACE_PATH 解析,
2247
+ // 与 validateBoundFilesExist 保持一致,避免 cwd ≠ 工作区时读不到文件。
2248
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
2249
+ const resolvedPath = path.resolve(workspacePath, filePath);
2250
+ // L-5: 防止路径逃逸(如 ../../../etc/passwd)
2251
+ const normalizedWorkspace = path.resolve(workspacePath);
2252
+ const normalizedResolved = path.resolve(resolvedPath);
2253
+ if (!normalizedResolved.startsWith(normalizedWorkspace + path.sep) && normalizedResolved !== normalizedWorkspace) {
2254
+ logger.warn(`[McpServer] calculateFileMd5: 路径逃逸被拒绝: ${filePath}`);
2255
+ return null;
2256
+ }
1844
2257
  return new Promise((resolve) => {
1845
2258
  const hash = createHash('md5');
1846
- const stream = createReadStream(filePath);
2259
+ const stream = createReadStream(resolvedPath);
1847
2260
  stream.on('data', (chunk) => hash.update(chunk));
1848
2261
  stream.on('end', () => resolve(hash.digest('hex')));
1849
2262
  stream.on('error', () => resolve(null));
@@ -1872,7 +2285,7 @@ export class McpServer {
1872
2285
  // - buffer 空、hydrate 从服务端拉到消息 → fromBuffer=false(来自服务端)
1873
2286
  // - buffer 空、hydrate 也无消息(仅有 Memory 通知注入)→ fromBuffer=true(本地注入)
1874
2287
  const hadBufferedMessages = this.messageBuffer.size() > 0;
1875
- await this.hydrateUnreadMessagesIfNeeded();
2288
+ await this.hydrateUnreadMessagesFromServer();
1876
2289
  const messages = this.messageBuffer.get();
1877
2290
  const hasRealMessages = messages.directMessages.length > 0 || messages.groupMessages.length > 0;
1878
2291
  const fromBuffer = hadBufferedMessages || !hasRealMessages;
@@ -1884,16 +2297,6 @@ export class McpServer {
1884
2297
  }
1885
2298
  return this.buildPollResult(messages, fromBuffer);
1886
2299
  }
1887
- /**
1888
- * 在本地缓冲区为空时,从服务端补拉离线期间积累的未读消息。
1889
- * 这样 Agent 重启或重连后,poll_message/get_message 仍能取到持久化未读消息。
1890
- */
1891
- async hydrateUnreadMessagesIfNeeded(options = {}) {
1892
- if (this.messageBuffer.size() > 0) {
1893
- return true;
1894
- }
1895
- return await this.hydrateUnreadMessagesFromServer(options);
1896
- }
1897
2300
  /**
1898
2301
  * 从服务端补拉未读消息并写入本地缓冲。
1899
2302
  * 主流程:按调用方指定的阻塞/流程线任务参数请求服务端 -> 过滤本地不可投递消息 -> 追加到当前消息缓存。
@@ -1921,7 +2324,7 @@ export class McpServer {
1921
2324
  return true;
1922
2325
  }
1923
2326
  catch (error) {
1924
- logger.error("[McpServer] 补拉未读消息失败:", error);
2327
+ logger.error('[McpServer] 补拉未读消息失败: %s, stack=%s', error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : '(no stack)');
1925
2328
  return false;
1926
2329
  }
1927
2330
  }
@@ -1994,6 +2397,12 @@ export class McpServer {
1994
2397
  */
1995
2398
  async handleIncomingMessage(message) {
1996
2399
  if (!this.shouldBufferIncomingMessage(message)) {
2400
+ // pipeline_task_assigned 不进 buffer,但需唤醒 poll 走 hydrate 重查任务,
2401
+ // 否则 poll_message 会被 messageBuffer.poll(30s) 阻塞,task 流转最长延迟 30s。
2402
+ if (!this.isGroupMessage(message) && this.isPipelineTaskAssignedNotification(message)) {
2403
+ logger.info("[McpServer] 收到 pipeline_task_assigned 唤醒信号,立即唤醒 poll 走 hydrate 重查");
2404
+ this.messageBuffer.wakeUpWaiters();
2405
+ }
1997
2406
  return undefined;
1998
2407
  }
1999
2408
  logger.info(`[McpServer] 收到消息: ${message.content?.substring(0, 50) || "(无内容)"}`);
@@ -2073,19 +2482,17 @@ export class McpServer {
2073
2482
  /**
2074
2483
  * 构建 Todo 未完成项提醒通知。
2075
2484
  *
2076
- * 触发条件(同时满足):
2485
+ * 触发条件(全部满足):
2077
2486
  * 1. 当前 agentId 可读取
2078
2487
  * 2. ~/.acode/todos/{agentId}.json 存在且可解析
2079
2488
  * 3. 存在 status !== "completed" 的未完成项
2080
- * 4. 未完成项内容/状态/优先级指纹与上次通知时不同(去重)
2081
2489
  *
2082
2490
  * 行为:
2083
- * - 指纹变化且有未完成项:构造系统通知并返回;指纹更新为本轮快照。
2084
- * - 指纹未变:返回 null(不重复通知)。
2085
- * - 全部完成:重置指纹为 null,下次有未完成项时能再次通知。
2086
- * - 任何异常(文件不存在/JSON 损坏/agentId 缺失):静默返回 null,不阻断 poll_message。
2491
+ * - 有未完成项:构造轻量系统通知并返回(不持久化)。
2492
+ * - 全部完成 / 无文件 / 异常:静默返回 null,不阻断 poll_message。
2087
2493
  *
2088
2494
  * 通知以 DirectMessage 形式注入,senderId="MCP-System",不落库、不持久化。
2495
+ * 提示内容简洁,仅提醒有未完成待办,Agent 如需查看详情应调用 todo_read。
2089
2496
  */
2090
2497
  async buildTodoReminderNotification() {
2091
2498
  try {
@@ -2114,20 +2521,8 @@ export class McpServer {
2114
2521
  t.status === "in_progress" ||
2115
2522
  t.status === "cancelled"));
2116
2523
  if (incomplete.length === 0) {
2117
- // 全完成(或无未完成项):重置指纹,下次有未完成项时能再次通知
2118
- this.lastTodoNotificationFingerprint = null;
2119
- return null;
2120
- }
2121
- const fingerprint = JSON.stringify(incomplete.map((t) => ({
2122
- c: t.content,
2123
- s: t.status,
2124
- p: t.priority,
2125
- })));
2126
- if (fingerprint === this.lastTodoNotificationFingerprint) {
2127
- // 指纹未变:不重复通知
2128
2524
  return null;
2129
2525
  }
2130
- this.lastTodoNotificationFingerprint = fingerprint;
2131
2526
  return {
2132
2527
  msgId: `todo-reminder-${Date.now()}`,
2133
2528
  senderId: "MCP-System",
@@ -2140,7 +2535,7 @@ export class McpServer {
2140
2535
  };
2141
2536
  }
2142
2537
  catch (error) {
2143
- logger.warn("[McpServer] 构建 Todo 提醒失败:", error);
2538
+ logger.warn('[McpServer] 构建 Todo 提醒失败: %s', error instanceof Error ? error.message : String(error));
2144
2539
  return null;
2145
2540
  }
2146
2541
  }
@@ -2159,53 +2554,64 @@ export class McpServer {
2159
2554
  // v1.13.0: 不再加载本地元数据缓存,过期/TODO/回忆状态统一由服务端 memory_status 字段管理
2160
2555
  // 后台注册:不阻塞 MCP 工具暴露,Agent 可立即接收工具调用
2161
2556
  this.autoRegisterInBackground();
2162
- // 启动后台 Memory 文件变更检测
2163
- this.startMemoryFileWatcher();
2164
2557
  logger.info("[McpServer] MCP Server 已就绪,工具已可用");
2165
2558
  }
2166
2559
  /**
2167
- * 停止 McpServer,清理后台定时器。
2168
- * 主流程:清理文件变更检测定时器 -> 停止 Agent 客户端消息循环。
2169
- */
2170
- stop() {
2171
- this.stopMemoryFileWatcher();
2172
- this.agentClient.stop();
2173
- logger.info("[McpServer] MCP Server 已停止");
2174
- }
2175
- /**
2176
- * 启动后台 Memory 文件变更检测定时器。
2177
- * 主流程:读取 AWS_MEMORY_FILE_CHECK_INTERVAL_MS -> 启动 setInterval -> 每次触发时检查绑定文件 MD5 变更 -> 有变更时直接推入消息缓冲区。
2178
- * 后台定时器启用后,poll_message 不再重复检查,避免重复推送通知。
2560
+ * 启动租约续期定时器 (v1.21.0)。
2561
+ * 60 秒续期一次当前 agent 持有的所有 updating memory 的租约,
2562
+ * 防止租约过期导致其他实例接管。
2179
2563
  */
2180
- startMemoryFileWatcher() {
2181
- const intervalMs = this.parsePositiveIntegerEnv("AWS_MEMORY_FILE_CHECK_INTERVAL_MS", DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS);
2182
- if (intervalMs <= 0) {
2183
- logger.info("[McpServer] 后台文件变更检测已禁用(AWS_MEMORY_FILE_CHECK_INTERVAL_MS=0),将回退到 poll_message 时检测");
2564
+ startLeaseRenewal() {
2565
+ if (this.leaseRenewalTimer)
2184
2566
  return;
2185
- }
2186
- this.memoryFileCheckInterval = setInterval(async () => {
2567
+ this.leaseRenewalTimer = setInterval(async () => {
2187
2568
  try {
2188
- const notification = await this.buildMemoryFileChangeNotification();
2189
- if (notification) {
2190
- this.messageBuffer.push(notification);
2191
- logger.info("[McpServer] 后台文件变更检测: 已推送 Memory 变更通知到消息缓冲区");
2569
+ const agentId = this.getCurrentAgentId();
2570
+ if (!agentId)
2571
+ return;
2572
+ const result = await this.memoryStore.renewMemoryLease(McpServer.LEASE_DURATION_MINUTES);
2573
+ // 续期返回 0 说明租约已丢失(被过期清理或被其他 agent 接管),停止续期
2574
+ if (result.renewed === 0) {
2575
+ logger.warn("[McpServer] 租约已丢失(renewed=0),停止续期定时器");
2576
+ this.stopLeaseRenewal();
2577
+ }
2578
+ // L-1: 主动清理 claimedCache 中的过期条目,避免堆积
2579
+ const now = Date.now();
2580
+ for (const [uri, expiry] of this.claimedCache) {
2581
+ if (expiry <= now) {
2582
+ this.claimedCache.delete(uri);
2583
+ }
2192
2584
  }
2193
2585
  }
2194
- catch (error) {
2195
- logger.warn("[McpServer] 后台文件变更检测失败:", error);
2586
+ catch (e) {
2587
+ logger.warn('[McpServer] 续期租约失败: %s', e instanceof Error ? e.message : String(e));
2196
2588
  }
2197
- }, intervalMs);
2198
- logger.info(`[McpServer] 后台文件变更检测已启动,间隔: ${intervalMs}ms(可通过 AWS_MEMORY_FILE_CHECK_INTERVAL_MS 环境变量调整,设为 0 禁用)`);
2589
+ }, McpServer.LEASE_RENEW_INTERVAL_MS);
2590
+ logger.info("[McpServer] 租约续期定时器已启动,间隔: 60s");
2199
2591
  }
2200
2592
  /**
2201
- * 停止后台 Memory 文件变更检测定时器。
2593
+ * 停止租约续期定时器 (v1.21.0)。
2202
2594
  */
2203
- stopMemoryFileWatcher() {
2204
- if (this.memoryFileCheckInterval) {
2205
- clearInterval(this.memoryFileCheckInterval);
2206
- this.memoryFileCheckInterval = null;
2207
- logger.info("[McpServer] 后台文件变更检测已停止");
2595
+ stopLeaseRenewal() {
2596
+ if (this.leaseRenewalTimer) {
2597
+ clearInterval(this.leaseRenewalTimer);
2598
+ this.leaseRenewalTimer = null;
2599
+ logger.info("[McpServer] 租约续期定时器已停止");
2600
+ }
2601
+ }
2602
+ /**
2603
+ * 停止 McpServer,清理后台定时器。
2604
+ * 主流程:停止租约续期 -> 释放所有 claim -> 停止 Agent 客户端消息循环。
2605
+ */
2606
+ stop() {
2607
+ this.stopLeaseRenewal();
2608
+ // 释放所有持有的 claim
2609
+ const agentId = this.getCurrentAgentId();
2610
+ if (agentId) {
2611
+ this.memoryStore.releaseAllMemoryClaims().catch(e => logger.warn('[McpServer] 释放 claim 失败: %s', e instanceof Error ? e.message : String(e)));
2208
2612
  }
2613
+ this.agentClient.stop();
2614
+ logger.info("[McpServer] MCP Server 已停止");
2209
2615
  }
2210
2616
  // ============ 记忆统一状态字段 (v1.13.0) ============
2211
2617
  // v1.13.0 改用服务端 memory_status 字段管理 agent_memory 过期/回忆状态,
@@ -2274,7 +2680,7 @@ export class McpServer {
2274
2680
  */
2275
2681
  autoRegisterInBackground() {
2276
2682
  this.autoRegister().catch((error) => {
2277
- logger.error("[McpServer] 后台注册异常:", error);
2683
+ logger.error('[McpServer] 后台注册异常: %s, stack=%s', error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : '(no stack)');
2278
2684
  });
2279
2685
  }
2280
2686
  /**
@@ -2282,30 +2688,52 @@ export class McpServer {
2282
2688
  */
2283
2689
  startListeningInternal() {
2284
2690
  logger.info("[McpServer] startListeningInternal 被调用");
2691
+ const profileHandler = (profile) => {
2692
+ // 收到 Profile 信息后,存储到消息缓冲区
2693
+ logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2694
+ this.messageBuffer.setProfileMessage(profile);
2695
+ this.profileSynced = true;
2696
+ this.syncRoleMemoryStore(profile);
2697
+ this.maybeSyncPermanentMemoryToServer();
2698
+ };
2285
2699
  // 降级模式下跳过 WebSocket 连接
2286
2700
  if (this.agentClient.isInDegradedMode()) {
2287
2701
  logger.info("[McpServer] 降级模式:跳过 WebSocket 连接,仅启动本地消息监听");
2288
2702
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2289
2703
  pollMode: false,
2290
- profileHandler: (profile) => {
2291
- // 收到 Profile 信息后,存储到消息缓冲区
2292
- logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2293
- this.messageBuffer.setProfileMessage(profile);
2294
- this.syncRoleMemoryStore(profile);
2295
- this.maybeSyncPermanentMemoryToServer();
2296
- },
2704
+ profileHandler,
2297
2705
  });
2706
+ this.activelySyncProfile();
2298
2707
  return;
2299
2708
  }
2300
2709
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2301
2710
  pollMode: false,
2302
- profileHandler: (profile) => {
2303
- // 收到 Profile 信息后,存储到消息缓冲区
2304
- logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
2305
- this.messageBuffer.setProfileMessage(profile);
2306
- this.syncRoleMemoryStore(profile);
2307
- this.maybeSyncPermanentMemoryToServer();
2308
- },
2711
+ profileHandler,
2712
+ });
2713
+ // 注册时 getProfileByAgentId 会触发 Java markProfileFetched,导致 WebSocket 连接后
2714
+ // Java 不再推送 PROFILE_INFO 消息(AgentWebSocketHandler 检查 hasProfileFetched 跳过)。
2715
+ // 因此 profileHandler 可能永远不会被触发,需要主动获取 profile 同步 hasRoleName。
2716
+ this.activelySyncProfile();
2717
+ }
2718
+ /**
2719
+ * 主动获取 profile 并同步角色存储路径。
2720
+ * 当 Java 侧因 hasProfileFetched=true 跳过 PROFILE_INFO 推送时,
2721
+ * profileHandler 不会被调用,hasRoleName 永远为 false,导致文件变更检测被跳过。
2722
+ * 此方法在 startListeningInternal 后主动拉取 profile,确保 hasRoleName 被正确设置。
2723
+ */
2724
+ activelySyncProfile() {
2725
+ if (this.profileSynced)
2726
+ return;
2727
+ this.agentClient.getProfile().then((profile) => {
2728
+ if (this.profileSynced)
2729
+ return;
2730
+ this.profileSynced = true;
2731
+ logger.info(`[McpServer] 主动获取 profile 成功: displayName=${profile.displayName}, roleName="${profile.roleName ?? '(空)'}"`);
2732
+ this.messageBuffer.setProfileMessage(profile);
2733
+ this.syncRoleMemoryStore(profile);
2734
+ this.maybeSyncPermanentMemoryToServer();
2735
+ }).catch((e) => {
2736
+ logger.warn('[McpServer] 主动获取 profile 失败: %s', e instanceof Error ? e.message : String(e));
2309
2737
  });
2310
2738
  }
2311
2739
  delay(ms) {