aws-runtime-bridge 1.9.40 → 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 (110) 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/runtime-binding.d.ts.map +1 -1
  17. package/dist/routes/runtime-binding.js +6 -3
  18. package/dist/routes/sessions.js +89 -4
  19. package/dist/routes/sub-agents.js +15 -1
  20. package/dist/routes/terminal.d.ts +3 -0
  21. package/dist/routes/terminal.d.ts.map +1 -1
  22. package/dist/routes/terminal.js +122 -5
  23. package/dist/services/acode-package.d.ts +8 -1
  24. package/dist/services/acode-package.d.ts.map +1 -1
  25. package/dist/services/acode-package.js +33 -4
  26. package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
  27. package/dist/services/aws-client-agent-mcp.js +11 -4
  28. package/dist/services/fileSnapshotStore.js +1 -1
  29. package/dist/services/legacy-path-migration.d.ts +3 -0
  30. package/dist/services/legacy-path-migration.d.ts.map +1 -0
  31. package/dist/services/legacy-path-migration.js +66 -0
  32. package/dist/services/lifecycle-state.d.ts.map +1 -1
  33. package/dist/services/lifecycle-state.js +1 -1
  34. package/dist/services/runtime-binding.d.ts +5 -0
  35. package/dist/services/runtime-binding.d.ts.map +1 -1
  36. package/dist/services/runtime-binding.js +33 -1
  37. package/dist/services/session-output.d.ts +7 -1
  38. package/dist/services/session-output.d.ts.map +1 -1
  39. package/dist/services/session-output.js +16 -2
  40. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  41. package/package/acode/dist/built-in-file-tools.js +504 -3
  42. package/package/acode/dist/compaction.d.ts +43 -7
  43. package/package/acode/dist/compaction.d.ts.map +1 -1
  44. package/package/acode/dist/compaction.js +286 -31
  45. package/package/acode/dist/runtime.d.ts +120 -0
  46. package/package/acode/dist/runtime.d.ts.map +1 -1
  47. package/package/acode/dist/runtime.js +921 -71
  48. package/package/acode/dist/sub-agent-manager.d.ts +96 -1
  49. package/package/acode/dist/sub-agent-manager.d.ts.map +1 -1
  50. package/package/acode/dist/sub-agent-manager.js +293 -3
  51. package/package/acode/dist/sub-agent-store.d.ts +8 -1
  52. package/package/acode/dist/sub-agent-store.d.ts.map +1 -1
  53. package/package/acode/dist/sub-agent-store.js +55 -0
  54. package/package/acode/dist/sub-agent-types.d.ts +4 -0
  55. package/package/acode/dist/sub-agent-types.d.ts.map +1 -1
  56. package/package/acode/dist/types.d.ts +19 -0
  57. package/package/acode/dist/types.d.ts.map +1 -1
  58. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +9 -0
  59. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  60. package/package/aws-client-agent-mcp/dist/agent-client.js +69 -10
  61. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  62. package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
  63. package/package/aws-client-agent-mcp/dist/config.js +4 -0
  64. package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
  65. package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
  66. package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
  67. package/package/aws-client-agent-mcp/dist/constants.js +3 -1
  68. package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
  69. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +4 -1
  70. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -1
  71. package/package/aws-client-agent-mcp/dist/context-manager.js +6 -2
  72. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -1
  73. package/package/aws-client-agent-mcp/dist/context-manager.test.js +5 -0
  74. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -1
  75. package/package/aws-client-agent-mcp/dist/http-client.d.ts +5 -0
  76. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  77. package/package/aws-client-agent-mcp/dist/http-client.js +15 -1
  78. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  79. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +92 -34
  80. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  81. package/package/aws-client-agent-mcp/dist/mcp-server.js +606 -208
  82. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  83. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +151 -51
  84. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  85. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +7 -0
  86. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  87. package/package/aws-client-agent-mcp/dist/mcp-tools.js +18 -0
  88. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  89. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +32 -9
  90. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  91. package/package/aws-client-agent-mcp/dist/memory-store.js +58 -19
  92. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  93. package/package/aws-client-agent-mcp/dist/memory-tools.js +3 -3
  94. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  95. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +13 -0
  96. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +1 -1
  97. package/package/aws-client-agent-mcp/dist/message-buffer.js +23 -0
  98. package/package/aws-client-agent-mcp/dist/message-buffer.js.map +1 -1
  99. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +13 -0
  100. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  101. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  102. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  103. package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
  104. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  105. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +34 -1
  107. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/websocket-client.js +146 -18
  109. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  110. 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 复合哈希。
@@ -62,8 +63,24 @@ export class McpServer {
62
63
  * 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
63
64
  */
64
65
  listableMd5Cache = new Map();
65
- /** 后台 Memory 文件变更检测定时器 (v1.9.0) */
66
- 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;
67
84
  /**
68
85
  * Memory 通知 null 结果缓存:key → expiresAt 时间戳。
69
86
  * 仅缓存 null(无通知)结果,避免 while(true) 每 30s 重复读盘。
@@ -76,12 +93,6 @@ export class McpServer {
76
93
  * 当已有 poll 在进行时,后续调用直接等待同一个 Promise。
77
94
  */
78
95
  pendingPollMessage = null;
79
- /**
80
- * Todo 未完成项提醒去重指纹:记录上次通知时的未完成项内容/状态/优先级快照。
81
- * 指纹一致则不重复通知;todos 变化时再次通知。
82
- * 进程重启后自动清空(届时会重新通知一次,可接受)。
83
- */
84
- lastTodoNotificationFingerprint = null;
85
96
  // 系统提示词由 runtime adapter(ACode/OpenCode/Codex/Claude)在会话创建时注入,
86
97
  // 无需通过 MCP 工具返回。三方 runtime(OpenCode/Claude)将三层提示词合并为一条 system prompt,
87
98
  // ACode 则直接注入为前三条 system role 消息。
@@ -90,7 +101,9 @@ export class McpServer {
90
101
  * 当服务端返回的 roleName 与进程初始化时不一致时,重建 roleMemoryStore 指向正确的目录。
91
102
  */
92
103
  syncRoleMemoryStore(profile) {
104
+ logger.info(`[McpServer] syncRoleMemoryStore 被调用: displayName=${profile.displayName}, roleName="${profile.roleName ?? '(空)'}", id=${profile.id}`);
93
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 })}`);
94
107
  return;
95
108
  }
96
109
  const expectedRoleDir = McpServer.sanitizeRoleDirectoryName(profile.roleName);
@@ -112,7 +125,7 @@ export class McpServer {
112
125
  */
113
126
  maybeSyncPermanentMemoryToServer() {
114
127
  this.doMaybeSyncPermanentMemoryToServer().catch((e) => {
115
- logger.warn("[McpServer] maybeSyncPermanentMemoryToServer 异常:", e);
128
+ logger.warn('[McpServer] maybeSyncPermanentMemoryToServer 异常: %s, stack=%s', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : '(no stack)');
116
129
  });
117
130
  }
118
131
  async doMaybeSyncPermanentMemoryToServer() {
@@ -285,7 +298,7 @@ export class McpServer {
285
298
  keywords: record.keywords,
286
299
  bound_files: record.bound_files,
287
300
  file_md5_map: record.file_md5_map,
288
- update_status: record.update_status,
301
+ memory_status: record.memory_status,
289
302
  updating_agent_id: record.updating_agent_id,
290
303
  last_check_time: record.last_check_time,
291
304
  };
@@ -300,6 +313,7 @@ export class McpServer {
300
313
  this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
301
314
  this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
302
315
  this.memoryStoresShareFile = false;
316
+ this.isSubAgent = process.env.AWS_SUB_AGENT === "true";
303
317
  this.contextManager = new ContextManager(this.sessionContextMemoryStore);
304
318
  this.server = new Server({
305
319
  name: MCP_SERVER_NAME,
@@ -313,7 +327,10 @@ export class McpServer {
313
327
  }
314
328
  setupHandlers() {
315
329
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
316
- 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 };
317
334
  });
318
335
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
319
336
  const { name, arguments: args } = request.params;
@@ -336,6 +353,8 @@ export class McpServer {
336
353
  ...error.details,
337
354
  }
338
355
  : { error: errorMessage };
356
+ // 服务端记录完整异常,方便排查 LOST 等异常状态
357
+ logger.error('[McpServer] 工具调用异常: tool=%s, error=%s, stack=%s', name, errorMessage, error instanceof Error ? error.stack : '(no stack)');
339
358
  return {
340
359
  content: [
341
360
  {
@@ -352,24 +371,44 @@ export class McpServer {
352
371
  * 处理工具调用
353
372
  */
354
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;
355
384
  // 报告工具调用状态
356
- const statusReporter = this.agentClient.getStatusReporter();
357
- if (statusReporter) {
358
- 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));
359
392
  }
360
393
  try {
361
- const result = await this.executeToolCall(name, args);
394
+ const result = await this.executeToolCall(name, cleanArgs);
362
395
  // 工具调用完成后,先上报返回数据供时间线日志查看,再切换为思考状态
363
396
  // (Agent 正在处理工具结果、决定下一步动作,而非真正的空闲等待)
364
- if (statusReporter) {
365
- statusReporter.reportToolResult(result);
366
- 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"状态
367
406
  }
368
407
  return result;
369
408
  }
370
409
  catch (error) {
371
410
  // 工具调用失败,报告错误状态
372
- if (statusReporter) {
411
+ if (statusReporter && !isSilentPoll) {
373
412
  statusReporter.reportToolError(error);
374
413
  }
375
414
  throw error;
@@ -379,6 +418,9 @@ export class McpServer {
379
418
  * 执行具体的工具调用
380
419
  */
381
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
+ }
382
424
  switch (name) {
383
425
  case "get_colleague":
384
426
  return await this.agentClient.discoverColleagues();
@@ -597,7 +639,17 @@ export class McpServer {
597
639
  return await this.memoryStore.readMemory(input);
598
640
  }
599
641
  if (input.memory_type) {
600
- return await this.getMemoryStoreForType(input.memory_type).readMemory(input);
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);
651
+ }
652
+ return null;
601
653
  }
602
654
  const longTermRecord = await this.roleMemoryStore.readMemory({
603
655
  ...input,
@@ -606,6 +658,14 @@ export class McpServer {
606
658
  if (longTermRecord) {
607
659
  return longTermRecord;
608
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;
668
+ }
609
669
  return await this.memoryStore.readMemory(input);
610
670
  }
611
671
  /**
@@ -617,10 +677,17 @@ export class McpServer {
617
677
  return { store: this.memoryStore, memory_type: input.memory_type };
618
678
  }
619
679
  if (input.memory_type) {
620
- return {
621
- store: this.getMemoryStoreForType(input.memory_type),
622
- memory_type: input.memory_type,
623
- };
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" };
689
+ }
690
+ return { store: localStore, memory_type: input.memory_type };
624
691
  }
625
692
  const longTermRecord = await this.roleMemoryStore.readMemory({
626
693
  uri: input.uri,
@@ -629,6 +696,14 @@ export class McpServer {
629
696
  if (longTermRecord) {
630
697
  return { store: this.roleMemoryStore, memory_type: "project_memory" };
631
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" };
706
+ }
632
707
  return { store: this.memoryStore };
633
708
  }
634
709
  /**
@@ -713,6 +788,43 @@ export class McpServer {
713
788
  }
714
789
  return sanitized;
715
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
+ }
716
828
  /**
717
829
  * 创建本地记忆。
718
830
  * 主流程:校验必填字段 -> 交给 MemoryStore 规范化 URI 和类型 -> 返回不含 content 的写入结果。
@@ -771,7 +883,9 @@ export class McpServer {
771
883
  }
772
884
  /**
773
885
  * 更新本地记忆。
774
- * 主流程:要求至少一种变更方式 -> MemoryStore 执行精确替换/追加和版本递增 -> 返回不含 content 的写入结果。
886
+ * 主流程:精确替换/追加/仅状态更新 -> MemoryStore 执行变更和版本递增 -> 返回不含 content 的写入结果。
887
+ * 支持三种写法:1) 精确替换(old_string + new_string);2) 追加(append);3) 无内容更新(仅传 uri,
888
+ * 用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景)。
775
889
  */
776
890
  async handleUpdateMemory(args) {
777
891
  const hasReplace = typeof args.old_string === "string" ||
@@ -782,10 +896,9 @@ export class McpServer {
782
896
  if (hasReplace && !hasCompleteReplace) {
783
897
  throw new Error("old_string and new_string must be provided together");
784
898
  }
785
- if (!hasCompleteReplace &&
786
- (typeof append !== "string" || append.length === 0)) {
787
- throw new Error("Provide either old_string/new_string or append");
788
- }
899
+ // 允许无内容更新:当未提供 old_string/new_string 且未提供有效 append 时,
900
+ // 视为"仅状态更新"——用于过期续期、刷新绑定文件 MD5、释放 updating 状态等场景。
901
+ // 此时内容不变,但会重算绑定文件 MD5 并续期(expirable 类型)。
789
902
  const input = {
790
903
  uri: this.getRequiredString(args, "uri"),
791
904
  memory_type: this.getOptionalMemoryType(args),
@@ -827,7 +940,8 @@ export class McpServer {
827
940
  });
828
941
  await this.reportCombinedMemoryStats();
829
942
  // Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
830
- if (target.memory_type === "project_memory") {
943
+ // target.store 已是服务端(兄弟实例创建的记忆回退场景)时跳过,避免重复写
944
+ if (target.memory_type === "project_memory" && target.store !== this.memoryStore) {
831
945
  try {
832
946
  await this.memoryStore.updateMemory({
833
947
  ...input,
@@ -841,9 +955,20 @@ export class McpServer {
841
955
  }
842
956
  }
843
957
  // v1.13.0: agent_memory 过期与回忆状态由服务端 memory_status 字段管理。
844
- // 显式传入的 recall_status / memory_status 已在 updateMemory 内部透传到服务端。
845
- // agent_memory 内容变更会触发 TTL ×2 续期(由 MemoryStore.updateMemory 内部处理),
846
- // recently_memory 续期建议重新 createMemory 替代。
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
+ }
847
972
  return this.toMemoryWriteResult(updated);
848
973
  }
849
974
  /**
@@ -866,7 +991,8 @@ export class McpServer {
866
991
  // v1.13.0: 服务端 deleteMemory 内部已处理相关清理逻辑,
867
992
  // TS 侧无需任何本地元数据清理。
868
993
  // Dual-write permanent memory delete to server
869
- if (effectiveType === "project_memory") {
994
+ // target.store 已是服务端时跳过,避免重复删除
995
+ if (effectiveType === "project_memory" && target.store !== this.memoryStore) {
870
996
  try {
871
997
  await this.memoryStore.deleteMemory({ uri: input.uri, memory_type: "project_memory" });
872
998
  }
@@ -1214,24 +1340,38 @@ export class McpServer {
1214
1340
  return merged;
1215
1341
  }
1216
1342
  /**
1217
- * 放弃 Memory 更新状态
1343
+ * 放弃 Memory 更新状态 (v1.21.0 改为走 release API)
1218
1344
  * Memory 增强功能 (v1.6.0)
1219
1345
  */
1220
1346
  async handleAbandonMemoryUpdate(args) {
1221
1347
  const uri = this.getRequiredString(args, "uri");
1222
1348
  const memoryType = this.getOptionalMemoryType(args);
1349
+ // 调 Server release API 释放 claim
1350
+ await this.memoryStore.releaseMemoryClaim(uri);
1351
+ // 同时清除本地 roleMemoryStore 的 updating 状态(如果是 project_memory)
1352
+ // 兄弟实例创建的记忆本地不存在时静默跳过,服务端 release 才是权威操作
1223
1353
  const record = await this.memoryStore.readMemory({ uri, memory_type: memoryType });
1224
- if (!record) {
1225
- 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();
1226
1371
  }
1227
- // 清除 updating 状态
1228
- await this.memoryStore.updateMemory({
1229
- uri: record.uri,
1230
- memory_type: record.memory_type,
1231
- });
1232
1372
  return {
1233
1373
  success: true,
1234
- uri: record.uri,
1374
+ uri,
1235
1375
  message: "Memory 更新状态已清除",
1236
1376
  };
1237
1377
  }
@@ -1273,7 +1413,7 @@ export class McpServer {
1273
1413
  await this.memoryStore.markExpiredMemoriesRecalling(batchUris);
1274
1414
  }
1275
1415
  catch (e) {
1276
- logger.warn("[McpServer] 标记过期记忆为 recalling 失败:", e);
1416
+ logger.warn('[McpServer] 标记过期记忆为 recalling 失败: uriCount=%s, error=%s', batchUris.length, e instanceof Error ? e.message : String(e));
1277
1417
  }
1278
1418
  const remaining = Math.max(0, expiredMemories.length - batch.length);
1279
1419
  // 返回过期记忆的摘要(不含完整 content 以减少上下文占用)
@@ -1282,7 +1422,6 @@ export class McpServer {
1282
1422
  title: m.title,
1283
1423
  domain: m.domain,
1284
1424
  path: m.path,
1285
- recall_status: 'recalling',
1286
1425
  memory_status: 'recalling',
1287
1426
  contentPreview: typeof m.content === 'string' && m.content.length > 200 ? m.content.substring(0, 200) + '...' : m.content,
1288
1427
  createdAt: m.createdAt,
@@ -1314,7 +1453,7 @@ export class McpServer {
1314
1453
  result = await this.memoryStore.clearRecallingExpiredMemories();
1315
1454
  }
1316
1455
  catch (e) {
1317
- logger.warn("[McpServer] 清理回忆中记忆失败:", e);
1456
+ logger.warn('[McpServer] 清理回忆中记忆失败: %s', e instanceof Error ? e.message : String(e));
1318
1457
  }
1319
1458
  await this.reportCombinedMemoryStats();
1320
1459
  const deletedCount = result.deleted ?? 0;
@@ -1435,21 +1574,12 @@ export class McpServer {
1435
1574
  return result;
1436
1575
  }
1437
1576
  async handlePollMessage() {
1577
+ logger.info('[McpServer] handlePollMessage 被调用');
1438
1578
  if (this.pendingPollMessage) {
1439
1579
  logger.info('[McpServer] poll_message 已有进行中的调用,等待其完成');
1440
1580
  return await this.pendingPollMessage;
1441
1581
  }
1442
1582
  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
1583
  return await this.doPollMessage();
1454
1584
  })();
1455
1585
  try {
@@ -1459,35 +1589,100 @@ export class McpServer {
1459
1589
  this.pendingPollMessage = null;
1460
1590
  }
1461
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
+ */
1462
1609
  async doPollMessage() {
1610
+ logger.info('[McpServer] doPollMessage 被调用');
1611
+ // 紧急类检查:hydrate 未读消息 + task
1463
1612
  await this.hydrateUnreadMessagesFromServer({
1464
1613
  blockIfEmpty: false,
1465
- includePipelineTasks: false,
1614
+ includePipelineTasks: true,
1466
1615
  });
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);
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);
1471
1623
  }
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) {
1472
1637
  const polled = await this.messageBuffer.poll(POLL_MESSAGE_INTERNAL_POLL_INTERVAL_MS);
1638
+ // 紧急类:poll 到消息(私信/群聊),立即返回
1473
1639
  if (polled.directMessages.length > 0 || polled.groupMessages.length > 0) {
1474
- return await this.finalizeWithMemoryNotifications(polled);
1640
+ return this.buildPollResult(polled);
1641
+ }
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);
1655
+ }
1656
+ // 紧急类:无消息/task 但有 todo,返回 todo
1657
+ if (todoNotifLoop) {
1658
+ return this.buildPollResult({ directMessages: [todoNotifLoop], groupMessages: [] });
1659
+ }
1660
+ // 非紧急类:无紧急类时,检查所有非紧急项
1661
+ const nonUrgent = await this.checkAllLocalNonUrgentNotifications();
1662
+ if (nonUrgent.length > 0) {
1663
+ return this.buildPollResult({ directMessages: nonUrgent, groupMessages: [] });
1475
1664
  }
1476
1665
  }
1477
1666
  }
1478
1667
  /**
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);
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;
1491
1686
  }
1492
1687
  buildPollResult(messages, fromBuffer = true) {
1493
1688
  return {
@@ -1556,10 +1751,14 @@ export class McpServer {
1556
1751
  requireReply: false,
1557
1752
  prompt: "这是一条本地 memory 容量整理提醒,自动生成,无需持久化。请优先整理已超过建议上限的短期/近期 memory 类型,或具体类型下的 memory 条目。",
1558
1753
  timestamp: new Date().toISOString(),
1754
+ _acodeAutoDelegate: {
1755
+ type: 'memory_cleanup',
1756
+ uris: exceededDomains.map(d => `${d.memoryType}/${d.domain}`),
1757
+ },
1559
1758
  };
1560
1759
  }
1561
1760
  catch (error) {
1562
- logger.warn("[McpServer] 构建本地 memory 整理提醒失败:", error);
1761
+ logger.warn('[McpServer] 构建本地 memory 整理提醒失败: %s', error instanceof Error ? error.message : String(error));
1563
1762
  return null;
1564
1763
  }
1565
1764
  }
@@ -1583,7 +1782,7 @@ export class McpServer {
1583
1782
  const agentId = this.getCurrentAgentId();
1584
1783
  if (!agentId)
1585
1784
  return null;
1586
- const recallingExpired = expiredMemories.filter(m => m.recall_status === 'recalling' || m.memory_status === 'recalling');
1785
+ const recallingExpired = expiredMemories.filter(m => m.memory_status === 'recalling');
1587
1786
  const normalExpired = expiredMemories.filter(m => !recallingExpired.includes(m));
1588
1787
  // 场景1: 有 recalling 状态的过期记忆(Agent 未完成疏理)
1589
1788
  if (recallingExpired.length > 0) {
@@ -1597,6 +1796,10 @@ export class McpServer {
1597
1796
  requireReply: false,
1598
1797
  prompt: "memory_recurrence 取回的记忆仍在回忆中,请通过 memory_recurrence_complete 完成疏理,或继续归档/续期。",
1599
1798
  timestamp: new Date().toISOString(),
1799
+ _acodeAutoDelegate: {
1800
+ type: 'memory_expiration',
1801
+ uris: recallingExpired.map(m => m.uri),
1802
+ },
1600
1803
  };
1601
1804
  }
1602
1805
  // 场景2: 有待回忆的过期记忆
@@ -1611,12 +1814,16 @@ export class McpServer {
1611
1814
  requireReply: false,
1612
1815
  prompt: `这是一条近期记忆过期提醒,共有 ${normalExpired.length} 条记忆即将遗忘,请调用 memory_recurrence 分批次获取并决定处理方式。`,
1613
1816
  timestamp: new Date().toISOString(),
1817
+ _acodeAutoDelegate: {
1818
+ type: 'memory_expiration',
1819
+ uris: normalExpired.map(m => m.uri),
1820
+ },
1614
1821
  };
1615
1822
  }
1616
1823
  return null;
1617
1824
  }
1618
1825
  catch (error) {
1619
- logger.warn("[McpServer] 构建近期记忆过期回忆通知失败:", error);
1826
+ logger.warn('[McpServer] 构建近期记忆过期回忆通知失败: %s', error instanceof Error ? error.message : String(error));
1620
1827
  return null;
1621
1828
  }
1622
1829
  }
@@ -1625,24 +1832,38 @@ export class McpServer {
1625
1832
  * Memory 增强功能 (v1.6.0)
1626
1833
  *
1627
1834
  * 检测流程:
1835
+ * 0. 同步 Server 的 claim 状态到本地,清理过期 claim
1628
1836
  * 1. 检查本 Agent 标记为 updating 但未处理的 Memory
1629
1837
  * 2. 检查短期记忆、近期记忆绑定的文件是否变更
1630
- * 3. 如果没有变更,检查永久记忆的文件变更,标记最多 5 个为 updating
1838
+ * 3. 检查永久记忆的文件变更,先走 Server 原子 claim,再标记本地
1631
1839
  */
1632
1840
  async buildMemoryFileChangeNotification() {
1841
+ logger.info(`[McpServer] buildMemoryFileChangeNotification 被调用: hasRoleName=${this.hasRoleName}, agentId=${this.getCurrentAgentId() ?? '(空)'}`);
1633
1842
  // 无角色名时不检测本地永久记忆文件变更(避免 default-role 跨角色污染)
1634
- if (!this.hasRoleName)
1843
+ if (!this.hasRoleName) {
1844
+ logger.info('[McpServer] 文件变更检测跳过: hasRoleName=false');
1635
1845
  return null;
1846
+ }
1636
1847
  try {
1637
1848
  const agentId = this.getCurrentAgentId();
1638
1849
  if (!agentId) {
1850
+ logger.info('[McpServer] 文件变更检测跳过: agentId 为空');
1639
1851
  return null;
1640
1852
  }
1641
- // 步骤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(服务端角色级永久记忆),
1642
1858
  // 同时检测绑定文件是否再次变更(用户可能在 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);
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);
1645
1863
  if (updatingMemories.length > 0) {
1864
+ // 当前 agent 持有未完成的 updating memory,确保租约续期定时器在运行,
1865
+ // 避免 agent 在更新过程中租约过期被其他实例接管。
1866
+ this.startLeaseRenewal();
1646
1867
  // 对 updating 记忆也检测文件是否再次变更
1647
1868
  const updatingChanges = await this.checkMemoriesFileChanges(updatingMemories);
1648
1869
  if (updatingChanges.length > 0) {
@@ -1650,10 +1871,11 @@ export class McpServer {
1650
1871
  for (const change of updatingChanges) {
1651
1872
  const memory = updatingMemories.find(m => m.id === change.memoryId);
1652
1873
  if (memory) {
1653
- await this.roleMemoryStore.updateMemory({
1874
+ // MD5 回写到服务端角色级 scope(显式传 memory_status:updating 防止 auto-release)
1875
+ await this.memoryStore.updateMemory({
1654
1876
  uri: memory.uri,
1655
1877
  memory_type: 'project_memory',
1656
- update_status: 'updating',
1878
+ memory_status: 'updating',
1657
1879
  updating_agent_id: agentId,
1658
1880
  file_md5_map: memory.file_md5_map,
1659
1881
  last_check_time: memory.last_check_time,
@@ -1674,12 +1896,17 @@ export class McpServer {
1674
1896
  requireReply: false,
1675
1897
  prompt: "这是一条文件变更提醒,您之前标记为更新中的记忆绑定的文件又有新变更,请处理。",
1676
1898
  timestamp: new Date().toISOString(),
1899
+ _acodeAutoDelegate: {
1900
+ type: 'memory_file_change',
1901
+ uris: updatingChanges.slice(0, 5).map(c => c.memoryUri),
1902
+ },
1677
1903
  };
1678
1904
  }
1679
1905
  const memoryList = updatingMemories
1680
1906
  .slice(0, 5)
1681
1907
  .map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
1682
1908
  .join('\n');
1909
+ logger.info(`[McpServer] 文件变更检测 Step1: ${updatingMemories.length} 个 updating memory 待更新,租约已续期`);
1683
1910
  return {
1684
1911
  msgId: `memory-update-pending-${Date.now()}`,
1685
1912
  senderId: "MCP-System",
@@ -1689,14 +1916,22 @@ export class McpServer {
1689
1916
  requireReply: false,
1690
1917
  prompt: "这是一条更新提醒,您之前已标记这些记忆为更新中,请继续处理。",
1691
1918
  timestamp: new Date().toISOString(),
1919
+ _acodeAutoDelegate: {
1920
+ type: 'memory_file_change',
1921
+ uris: updatingMemories.slice(0, 5).map(m => m.uri),
1922
+ },
1692
1923
  };
1693
1924
  }
1694
1925
  // 步骤2: 检查短期记忆和近期记忆(服务端存储)
1695
1926
  const recentlyMemoryMemories = await this.memoryStore.listByType('recently_memory');
1696
1927
  const agentMemoryMemories = await this.memoryStore.listByType('agent_memory');
1697
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'}`);
1698
1934
  // 用本地 MD5 缓存补全 listable 记忆(服务端可能不持久化 file_md5_map 字段)
1699
- // 确保 checkMemoriesFileChanges 有正确的旧 MD5 做比对而非始终以空字典开始
1700
1935
  for (const m of listableMemories) {
1701
1936
  if (!m.file_md5_map) {
1702
1937
  const cached = this.listableMd5Cache.get(m.id);
@@ -1706,17 +1941,20 @@ export class McpServer {
1706
1941
  }
1707
1942
  }
1708
1943
  const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
1709
- // 使用复合 key(memoryId + 当前各绑定文件 MD5)过滤已通知变更,
1710
- // 同一条记忆文件再次变更(MD5 变化)时仍能触发新通知
1711
1944
  const newListableChanges = listableChanges.filter(c => {
1712
1945
  const memory = listableMemories.find(m => m.id === c.memoryId);
1713
1946
  const md5Map = memory?.file_md5_map;
1714
1947
  const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
1715
- 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;
1716
1954
  });
1955
+ logger.info(`[McpServer] 文件变更检测 Step2: 检测到变更=${listableChanges.length}, 去重后=${newListableChanges.length}` +
1956
+ (listableChanges.length > 0 ? ` 变更项: ${listableChanges.map(c => `${c.memoryUri}(${c.changedFiles.join(',')})`).join('; ')}` : ''));
1717
1957
  if (newListableChanges.length > 0) {
1718
- // 记录当前状态的复合键,下次对比时若 MD5 变化则重新通知
1719
- // 同时将 MD5 map 写入本地缓存,弥补服务端不持久化 file_md5_map 的问题
1720
1958
  for (const c of newListableChanges) {
1721
1959
  const memory = listableMemories.find(m => m.id === c.memoryId);
1722
1960
  const md5Map = memory?.file_md5_map;
@@ -1737,52 +1975,164 @@ export class McpServer {
1737
1975
  requireReply: false,
1738
1976
  prompt: "这是一条文件变更提醒,自动生成,无需持久化。",
1739
1977
  timestamp: new Date().toISOString(),
1978
+ _acodeAutoDelegate: {
1979
+ type: 'memory_file_change',
1980
+ uris: newListableChanges.map(c => c.memoryUri),
1981
+ },
1740
1982
  };
1741
1983
  }
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);
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('; ')}` : ''));
1746
2014
  if (permanentChanges.length > 0) {
1747
2015
  // 最多选取 5 个
1748
2016
  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
- });
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
+ }
2050
+ }
2051
+ // 启动租约续期(仅 Server claim 成功时需要,降级模式不续期)
2052
+ if (claimedViaServer && claimedUris.size > 0) {
2053
+ this.startLeaseRenewal();
1763
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
+ };
1764
2072
  }
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
2073
  }
2074
+ logger.info('[McpServer] 文件变更检测完成: 无变更通知产生');
1779
2075
  return null;
1780
2076
  }
1781
2077
  catch (error) {
1782
- logger.warn("[McpServer] 构建 Memory 文件变更检测通知失败:", error);
2078
+ logger.warn('[McpServer] 构建 Memory 文件变更检测通知失败: %s', error instanceof Error ? error.message : String(error));
1783
2079
  return null;
1784
2080
  }
1785
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
+ }
1786
2136
  /**
1787
2137
  * 检查一组 Memory 的文件变更
1788
2138
  * Memory 增强功能 (v1.6.0)
@@ -1797,25 +2147,47 @@ export class McpServer {
1797
2147
  const changedFiles = [];
1798
2148
  const newMd5Map = {};
1799
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;
1800
2152
  for (const filePath of memory.bound_files) {
1801
2153
  try {
1802
2154
  const currentMd5 = await this.calculateFileMd5(filePath);
1803
2155
  if (currentMd5 === null) {
1804
2156
  // 文件不存在或无法读取
1805
- 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
+ }
1806
2163
  continue;
1807
2164
  }
1808
2165
  newMd5Map[filePath] = currentMd5;
1809
2166
  const oldMd5 = oldMd5Map[filePath];
1810
2167
  if (!oldMd5 || oldMd5 !== currentMd5) {
1811
- 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
+ }
1812
2176
  }
1813
2177
  }
1814
2178
  catch (error) {
1815
2179
  logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
1816
- changedFiles.push(`${filePath} (MD5计算失败)`);
2180
+ if (!hasNoBaseline) {
2181
+ changedFiles.push(`${filePath} (MD5计算失败)`);
2182
+ }
1817
2183
  }
1818
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
+ }
1819
2191
  if (changedFiles.length > 0) {
1820
2192
  changes.push({
1821
2193
  memoryId: memory.id,
@@ -1841,9 +2213,20 @@ export class McpServer {
1841
2213
  try {
1842
2214
  const { createReadStream } = await import('node:fs');
1843
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
+ }
1844
2227
  return new Promise((resolve) => {
1845
2228
  const hash = createHash('md5');
1846
- const stream = createReadStream(filePath);
2229
+ const stream = createReadStream(resolvedPath);
1847
2230
  stream.on('data', (chunk) => hash.update(chunk));
1848
2231
  stream.on('end', () => resolve(hash.digest('hex')));
1849
2232
  stream.on('error', () => resolve(null));
@@ -1872,7 +2255,7 @@ export class McpServer {
1872
2255
  // - buffer 空、hydrate 从服务端拉到消息 → fromBuffer=false(来自服务端)
1873
2256
  // - buffer 空、hydrate 也无消息(仅有 Memory 通知注入)→ fromBuffer=true(本地注入)
1874
2257
  const hadBufferedMessages = this.messageBuffer.size() > 0;
1875
- await this.hydrateUnreadMessagesIfNeeded();
2258
+ await this.hydrateUnreadMessagesFromServer();
1876
2259
  const messages = this.messageBuffer.get();
1877
2260
  const hasRealMessages = messages.directMessages.length > 0 || messages.groupMessages.length > 0;
1878
2261
  const fromBuffer = hadBufferedMessages || !hasRealMessages;
@@ -1884,16 +2267,6 @@ export class McpServer {
1884
2267
  }
1885
2268
  return this.buildPollResult(messages, fromBuffer);
1886
2269
  }
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
2270
  /**
1898
2271
  * 从服务端补拉未读消息并写入本地缓冲。
1899
2272
  * 主流程:按调用方指定的阻塞/流程线任务参数请求服务端 -> 过滤本地不可投递消息 -> 追加到当前消息缓存。
@@ -1921,7 +2294,7 @@ export class McpServer {
1921
2294
  return true;
1922
2295
  }
1923
2296
  catch (error) {
1924
- 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)');
1925
2298
  return false;
1926
2299
  }
1927
2300
  }
@@ -1994,6 +2367,12 @@ export class McpServer {
1994
2367
  */
1995
2368
  async handleIncomingMessage(message) {
1996
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
+ }
1997
2376
  return undefined;
1998
2377
  }
1999
2378
  logger.info(`[McpServer] 收到消息: ${message.content?.substring(0, 50) || "(无内容)"}`);
@@ -2073,19 +2452,17 @@ export class McpServer {
2073
2452
  /**
2074
2453
  * 构建 Todo 未完成项提醒通知。
2075
2454
  *
2076
- * 触发条件(同时满足):
2455
+ * 触发条件(全部满足):
2077
2456
  * 1. 当前 agentId 可读取
2078
2457
  * 2. ~/.acode/todos/{agentId}.json 存在且可解析
2079
2458
  * 3. 存在 status !== "completed" 的未完成项
2080
- * 4. 未完成项内容/状态/优先级指纹与上次通知时不同(去重)
2081
2459
  *
2082
2460
  * 行为:
2083
- * - 指纹变化且有未完成项:构造系统通知并返回;指纹更新为本轮快照。
2084
- * - 指纹未变:返回 null(不重复通知)。
2085
- * - 全部完成:重置指纹为 null,下次有未完成项时能再次通知。
2086
- * - 任何异常(文件不存在/JSON 损坏/agentId 缺失):静默返回 null,不阻断 poll_message。
2461
+ * - 有未完成项:构造轻量系统通知并返回(不持久化)。
2462
+ * - 全部完成 / 无文件 / 异常:静默返回 null,不阻断 poll_message。
2087
2463
  *
2088
2464
  * 通知以 DirectMessage 形式注入,senderId="MCP-System",不落库、不持久化。
2465
+ * 提示内容简洁,仅提醒有未完成待办,Agent 如需查看详情应调用 todo_read。
2089
2466
  */
2090
2467
  async buildTodoReminderNotification() {
2091
2468
  try {
@@ -2114,20 +2491,8 @@ export class McpServer {
2114
2491
  t.status === "in_progress" ||
2115
2492
  t.status === "cancelled"));
2116
2493
  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
2494
  return null;
2129
2495
  }
2130
- this.lastTodoNotificationFingerprint = fingerprint;
2131
2496
  return {
2132
2497
  msgId: `todo-reminder-${Date.now()}`,
2133
2498
  senderId: "MCP-System",
@@ -2140,7 +2505,7 @@ export class McpServer {
2140
2505
  };
2141
2506
  }
2142
2507
  catch (error) {
2143
- logger.warn("[McpServer] 构建 Todo 提醒失败:", error);
2508
+ logger.warn('[McpServer] 构建 Todo 提醒失败: %s', error instanceof Error ? error.message : String(error));
2144
2509
  return null;
2145
2510
  }
2146
2511
  }
@@ -2159,54 +2524,65 @@ export class McpServer {
2159
2524
  // v1.13.0: 不再加载本地元数据缓存,过期/TODO/回忆状态统一由服务端 memory_status 字段管理
2160
2525
  // 后台注册:不阻塞 MCP 工具暴露,Agent 可立即接收工具调用
2161
2526
  this.autoRegisterInBackground();
2162
- // 启动后台 Memory 文件变更检测
2163
- this.startMemoryFileWatcher();
2164
2527
  logger.info("[McpServer] MCP Server 已就绪,工具已可用");
2165
2528
  }
2166
2529
  /**
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 不再重复检查,避免重复推送通知。
2530
+ * 启动租约续期定时器 (v1.21.0)。
2531
+ * 60 秒续期一次当前 agent 持有的所有 updating memory 的租约,
2532
+ * 防止租约过期导致其他实例接管。
2179
2533
  */
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 时检测");
2534
+ startLeaseRenewal() {
2535
+ if (this.leaseRenewalTimer)
2184
2536
  return;
2185
- }
2186
- this.memoryFileCheckInterval = setInterval(async () => {
2537
+ this.leaseRenewalTimer = setInterval(async () => {
2187
2538
  try {
2188
- const notification = await this.buildMemoryFileChangeNotification();
2189
- if (notification) {
2190
- this.messageBuffer.push(notification);
2191
- 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
+ }
2192
2554
  }
2193
2555
  }
2194
- catch (error) {
2195
- logger.warn("[McpServer] 后台文件变更检测失败:", error);
2556
+ catch (e) {
2557
+ logger.warn('[McpServer] 续期租约失败: %s', e instanceof Error ? e.message : String(e));
2196
2558
  }
2197
- }, intervalMs);
2198
- logger.info(`[McpServer] 后台文件变更检测已启动,间隔: ${intervalMs}ms(可通过 AWS_MEMORY_FILE_CHECK_INTERVAL_MS 环境变量调整,设为 0 禁用)`);
2559
+ }, McpServer.LEASE_RENEW_INTERVAL_MS);
2560
+ logger.info("[McpServer] 租约续期定时器已启动,间隔: 60s");
2199
2561
  }
2200
2562
  /**
2201
- * 停止后台 Memory 文件变更检测定时器。
2563
+ * 停止租约续期定时器 (v1.21.0)。
2202
2564
  */
2203
- stopMemoryFileWatcher() {
2204
- if (this.memoryFileCheckInterval) {
2205
- clearInterval(this.memoryFileCheckInterval);
2206
- this.memoryFileCheckInterval = null;
2207
- logger.info("[McpServer] 后台文件变更检测已停止");
2565
+ stopLeaseRenewal() {
2566
+ if (this.leaseRenewalTimer) {
2567
+ clearInterval(this.leaseRenewalTimer);
2568
+ this.leaseRenewalTimer = null;
2569
+ logger.info("[McpServer] 租约续期定时器已停止");
2208
2570
  }
2209
2571
  }
2572
+ /**
2573
+ * 停止 McpServer,清理后台定时器。
2574
+ * 主流程:停止租约续期 -> 释放所有 claim -> 停止 Agent 客户端消息循环。
2575
+ */
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)));
2582
+ }
2583
+ this.agentClient.stop();
2584
+ logger.info("[McpServer] MCP Server 已停止");
2585
+ }
2210
2586
  // ============ 记忆统一状态字段 (v1.13.0) ============
2211
2587
  // v1.13.0 改用服务端 memory_status 字段管理 agent_memory 过期/回忆状态,
2212
2588
  // 下方工具直接调用服务端接口,本地不再维护元数据缓存。
@@ -2274,7 +2650,7 @@ export class McpServer {
2274
2650
  */
2275
2651
  autoRegisterInBackground() {
2276
2652
  this.autoRegister().catch((error) => {
2277
- 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)');
2278
2654
  });
2279
2655
  }
2280
2656
  /**
@@ -2282,30 +2658,52 @@ export class McpServer {
2282
2658
  */
2283
2659
  startListeningInternal() {
2284
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
+ };
2285
2669
  // 降级模式下跳过 WebSocket 连接
2286
2670
  if (this.agentClient.isInDegradedMode()) {
2287
2671
  logger.info("[McpServer] 降级模式:跳过 WebSocket 连接,仅启动本地消息监听");
2288
2672
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2289
2673
  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
- },
2674
+ profileHandler,
2297
2675
  });
2676
+ this.activelySyncProfile();
2298
2677
  return;
2299
2678
  }
2300
2679
  this.agentClient.start(this.handleIncomingMessage.bind(this), {
2301
2680
  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
- },
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));
2309
2707
  });
2310
2708
  }
2311
2709
  delay(ms) {