aws-runtime-bridge 1.9.10 → 1.9.20

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 (121) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +40 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +353 -7
  4. package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
  5. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/OpencodeSdkAdapter.js +36 -3
  7. package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
  8. package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
  9. package/dist/adapter/instance-ai-config-resolver.js +46 -0
  10. package/dist/adapter/types.d.ts +27 -2
  11. package/dist/adapter/types.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/routes/dashboard.d.ts.map +1 -1
  14. package/dist/routes/dashboard.js +749 -15
  15. package/dist/routes/events.d.ts +7 -0
  16. package/dist/routes/events.d.ts.map +1 -0
  17. package/dist/routes/events.js +22 -0
  18. package/dist/routes/file-browser.d.ts +1 -1
  19. package/dist/routes/file-browser.d.ts.map +1 -1
  20. package/dist/routes/file-browser.js +25 -9
  21. package/dist/routes/file-snapshot.d.ts +10 -0
  22. package/dist/routes/file-snapshot.d.ts.map +1 -0
  23. package/dist/routes/file-snapshot.js +156 -0
  24. package/dist/routes/git.d.ts +10 -0
  25. package/dist/routes/git.d.ts.map +1 -1
  26. package/dist/routes/git.js +305 -2
  27. package/dist/routes/git.test.js +39 -2
  28. package/dist/routes/instance.js +3 -1
  29. package/dist/routes/memory.d.ts +3 -0
  30. package/dist/routes/memory.d.ts.map +1 -1
  31. package/dist/routes/memory.js +3 -0
  32. package/dist/routes/terminal.d.ts +2 -1
  33. package/dist/routes/terminal.d.ts.map +1 -1
  34. package/dist/routes/terminal.js +378 -41
  35. package/dist/routes/terminal.test.js +33 -7
  36. package/dist/services/agent-event-store.d.ts +59 -0
  37. package/dist/services/agent-event-store.d.ts.map +1 -0
  38. package/dist/services/agent-event-store.js +68 -0
  39. package/dist/services/event-delta-pusher.d.ts +40 -0
  40. package/dist/services/event-delta-pusher.d.ts.map +1 -0
  41. package/dist/services/event-delta-pusher.js +95 -0
  42. package/dist/services/fileSnapshotStore.d.ts +96 -0
  43. package/dist/services/fileSnapshotStore.d.ts.map +1 -0
  44. package/dist/services/fileSnapshotStore.js +500 -0
  45. package/dist/services/instance-ai-config-service.d.ts +49 -0
  46. package/dist/services/instance-ai-config-service.d.ts.map +1 -0
  47. package/dist/services/instance-ai-config-service.js +174 -0
  48. package/dist/services/panel-auth.d.ts +8 -0
  49. package/dist/services/panel-auth.d.ts.map +1 -1
  50. package/dist/services/panel-auth.js +8 -0
  51. package/dist/services/ring-buffer.d.ts +67 -0
  52. package/dist/services/ring-buffer.d.ts.map +1 -0
  53. package/dist/services/ring-buffer.js +103 -0
  54. package/dist/services/session-output.d.ts +47 -3
  55. package/dist/services/session-output.d.ts.map +1 -1
  56. package/dist/services/session-output.js +112 -2
  57. package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
  58. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
  59. package/dist/utils/instance-ai-config-debug-logger.js +63 -0
  60. package/dist/utils/logger.d.ts +18 -0
  61. package/dist/utils/logger.d.ts.map +1 -1
  62. package/dist/utils/logger.js +48 -4
  63. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  64. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  65. package/package/acode/dist/built-in-file-tools.js +5 -3
  66. package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
  67. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
  68. package/package/acode/dist/builtins/commandArtifacts.js +24 -4
  69. package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
  70. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
  71. package/package/acode/dist/builtins/shellCommandResult.js +3 -3
  72. package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
  73. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
  74. package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
  75. package/package/acode/dist/cli.js +2 -0
  76. package/package/acode/dist/runtime.d.ts.map +1 -1
  77. package/package/acode/dist/runtime.js +156 -5
  78. package/package/acode/dist/types.d.ts +1 -0
  79. package/package/acode/dist/types.d.ts.map +1 -1
  80. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
  81. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  82. package/package/aws-client-agent-mcp/dist/agent-client.js +16 -0
  83. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  84. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
  85. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
  86. package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
  87. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
  88. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
  89. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
  90. package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
  91. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
  92. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +170 -2
  93. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  94. package/package/aws-client-agent-mcp/dist/mcp-server.js +947 -9
  95. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  96. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +103 -1
  97. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  98. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
  99. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  100. package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
  101. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  102. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +46 -1
  103. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  104. package/package/aws-client-agent-mcp/dist/memory-store.js +164 -12
  105. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
  107. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/memory-tools.js +88 -1
  109. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  110. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
  111. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  112. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  113. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  114. package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
  115. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  116. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
  118. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/websocket-client.js +122 -5
  120. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  121. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  * MCP Server 实现
3
3
  * 提供 MCP Tools 供 AI Agent 调用
4
4
  */
5
+ import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -11,6 +12,7 @@ import { MCP_SERVER_NAME, MCP_SERVER_VERSION, REGISTER_MAX_RETRIES, REGISTER_RET
11
12
  import { logger } from "./logger.js";
12
13
  import { MCP_TOOLS } from "./mcp-tools.js";
13
14
  import { MessageBuffer } from "./message-buffer.js";
15
+ import { ContextManager } from "./context-manager.js";
14
16
  import { MemoryCapacityError, MemoryStore, } from "./memory-store.js";
15
17
  import { ServerMemoryStore } from "./server-memory-store.js";
16
18
  const DEFAULT_POLL_MESSAGE_BLOCK_WAIT_MS = 30_000;
@@ -18,6 +20,7 @@ const DEFAULT_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS = 100;
18
20
  const DEFAULT_POLL_MESSAGE_ERROR_RETRY_DELAY_MS = 5_000;
19
21
  const DEFAULT_MCP_HTTP_REQUEST_TIMEOUT_MS = 120_000;
20
22
  const POLL_MESSAGE_HTTP_TIMEOUT_BUFFER_MS = 1_000;
23
+ const DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS = 5_000;
21
24
  const PATH_UNSAFE_CHARACTERS = new Set([
22
25
  "<",
23
26
  ">",
@@ -35,7 +38,92 @@ export class McpServer {
35
38
  messageBuffer;
36
39
  memoryStore;
37
40
  roleMemoryStore;
41
+ sessionContextMemoryStore;
38
42
  memoryStoresShareFile;
43
+ initialRoleName = "";
44
+ /** 上下文管理器 (v1.8.0) */
45
+ contextManager;
46
+ /**
47
+ * 内存级去重缓存:记录本轮已通知过的 listable memory 文件变更状态。
48
+ * key = memoryId,value = 已通知时刻各绑定文件的 MD5 复合哈希。
49
+ * 文件再次变更(MD5 变化)时仍会触发新通知。
50
+ * 进程重启后自动清空(届时重新检测)。
51
+ */
52
+ notifiedFileChangeMemoryIds = new Map();
53
+ /**
54
+ * 本地缓存 listable 记忆(task_list/recently_memory)的文件 MD5 map。
55
+ * 服务端(Java 后端)不持久化 file_md5_map 字段,listByType 返回的记录该字段为 undefined。
56
+ * 必须本地缓存才能让 checkMemoriesFileChanges 有正确的旧 MD5 做对比。
57
+ */
58
+ listableMd5Cache = new Map();
59
+ /** 后台 Memory 文件变更检测定时器 (v1.9.0) */
60
+ memoryFileCheckInterval = null;
61
+ /**
62
+ * 收到 Profile 后同步角色存储路径。
63
+ * 当服务端返回的 roleName 与进程初始化时不一致时,重建 roleMemoryStore 指向正确的目录。
64
+ */
65
+ syncRoleMemoryStore(profile) {
66
+ if (!profile.roleName) {
67
+ return;
68
+ }
69
+ const expectedRoleDir = McpServer.sanitizeRoleDirectoryName(profile.roleName);
70
+ if (expectedRoleDir === this.initialRoleName) {
71
+ return;
72
+ }
73
+ logger.info(`[McpServer] 角色目录与初始值不一致,重建 roleMemoryStore: "${this.initialRoleName}" -> "${expectedRoleDir}"`);
74
+ this.roleMemoryStore = new MemoryStore(McpServer.buildRoleMemoryStorePath(expectedRoleDir));
75
+ this.initialRoleName = expectedRoleDir;
76
+ }
77
+ /**
78
+ * 规范化绑定的文件路径。
79
+ * 项目约定以下前缀都表示工作区根路径:
80
+ * ./xxx → ./xxx (已是相对路径)
81
+ * /xxx → ./xxx (/ 视为工作区根路径)
82
+ * @/xxx → ./xxx (@/ 视为工作区根路径别名)
83
+ * xxx → ./xxx (裸路径视为当前工作区相对)
84
+ */
85
+ normalizeBoundFilePath(filePath) {
86
+ const trimmed = String(filePath || "").trim();
87
+ if (!trimmed)
88
+ return trimmed;
89
+ // 去掉 @/ 前缀(@ 视为工作区根路径)
90
+ const noAt = trimmed.startsWith("@/") ? trimmed.substring(1) : trimmed;
91
+ if (noAt.startsWith("./"))
92
+ return noAt;
93
+ // /xxx 和 xxx 都统一成 ./xxx
94
+ return `./${noAt.replace(/^\//, "")}`;
95
+ }
96
+ /**
97
+ * 规范化绑定的文件路径数组。
98
+ */
99
+ normalizeBoundFiles(files) {
100
+ if (!files || !Array.isArray(files))
101
+ return files;
102
+ return files.map(f => this.normalizeBoundFilePath(String(f)));
103
+ }
104
+ /**
105
+ * 验证绑定文件是否存在。
106
+ * 主流程:规范化后的路径 -> 相对于工作区根路径解析 -> 逐文件检查是否存在 -> 汇总缺失列表并抛出错误。
107
+ */
108
+ async validateBoundFilesExist(boundFiles) {
109
+ if (!boundFiles || boundFiles.length === 0)
110
+ return;
111
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
112
+ const { access } = await import('node:fs/promises');
113
+ const missingFiles = [];
114
+ for (const filePath of boundFiles) {
115
+ try {
116
+ const resolvedPath = path.resolve(workspacePath, filePath);
117
+ await access(resolvedPath);
118
+ }
119
+ catch {
120
+ missingFiles.push(filePath);
121
+ }
122
+ }
123
+ if (missingFiles.length > 0) {
124
+ throw new Error(`绑定的文件不存在,请确认文件路径正确后再重新提交:\n${missingFiles.map(f => ` - ${f}`).join('\n')}`);
125
+ }
126
+ }
39
127
  /**
40
128
  * 记忆写入成功后的 MCP 响应 DTO。
41
129
  * 主流程:保留定位与版本元数据,刻意不包含 content,避免 create/update 回显大段提交内容。
@@ -46,19 +134,50 @@ export class McpServer {
46
134
  uri: record.uri,
47
135
  domain: record.domain,
48
136
  path: record.path,
137
+ title: record.title,
138
+ memory_type: record.memory_type,
139
+ createdAt: record.createdAt,
140
+ updatedAt: record.updatedAt,
141
+ version: record.version,
142
+ keywords: record.keywords,
143
+ bound_files: record.bound_files,
144
+ };
145
+ }
146
+ /**
147
+ * 返回完整的 Memory 记录(包含元数据)。
148
+ * Memory 增强功能 (v1.6.0)
149
+ */
150
+ toMemoryRecordWithMetadata(record) {
151
+ return {
152
+ id: record.id,
153
+ uri: record.uri,
154
+ domain: record.domain,
155
+ path: record.path,
156
+ title: record.title,
157
+ content: record.content,
49
158
  memory_type: record.memory_type,
50
159
  createdAt: record.createdAt,
51
160
  updatedAt: record.updatedAt,
52
161
  version: record.version,
162
+ // Memory 增强字段
163
+ keywords: record.keywords,
164
+ bound_files: record.bound_files,
165
+ file_md5_map: record.file_md5_map,
166
+ update_status: record.update_status,
167
+ updating_agent_id: record.updating_agent_id,
168
+ last_check_time: record.last_check_time,
53
169
  };
54
170
  }
55
171
  constructor() {
56
172
  this.agentClient = new AgentClient();
57
173
  this.messageBuffer = new MessageBuffer();
174
+ this.initialRoleName = McpServer.resolveRoleMemoryDirectoryName();
58
175
  const memoryStorePaths = McpServer.resolveMemoryStorePaths();
59
176
  this.memoryStore = new ServerMemoryStore(this.agentClient);
60
177
  this.roleMemoryStore = new MemoryStore(memoryStorePaths.rolePath);
178
+ this.sessionContextMemoryStore = new MemoryStore(path.join(path.dirname(memoryStorePaths.rolePath), "..", "session_context"));
61
179
  this.memoryStoresShareFile = false;
180
+ this.contextManager = new ContextManager(this.sessionContextMemoryStore);
62
181
  this.server = new Server({
63
182
  name: MCP_SERVER_NAME,
64
183
  version: MCP_SERVER_VERSION,
@@ -197,6 +316,16 @@ export class McpServer {
197
316
  return await this.handleRemoveTaskListMemory(args);
198
317
  case "archive_recently_memory":
199
318
  return await this.handleArchiveRecentlyMemory(args);
319
+ // Memory 增强功能工具 (v1.6.0)
320
+ case "list_memories_by_file":
321
+ return await this.handleListMemoriesByFile(args);
322
+ case "abandon_memory_update":
323
+ return await this.handleAbandonMemoryUpdate(args);
324
+ // 上下文压缩工具 (v1.8.0)
325
+ case "get_context_stats":
326
+ return await this.handleGetContextStats(args);
327
+ case "compact_context":
328
+ return await this.handleCompactContext(args);
200
329
  default:
201
330
  throw new Error(`Unknown tool: ${name}`);
202
331
  }
@@ -206,6 +335,7 @@ export class McpServer {
206
335
  "task_list",
207
336
  "recently_memory",
208
337
  "memory",
338
+ "session_context",
209
339
  ];
210
340
  static MEMORY_TOOL_NAMES = new Set([
211
341
  "create_memory",
@@ -217,14 +347,33 @@ export class McpServer {
217
347
  "get_memory_stats",
218
348
  "remove_task_list_memory",
219
349
  "archive_recently_memory",
350
+ // Memory 增强功能工具 (v1.6.0)
351
+ "list_memories_by_file",
352
+ "abandon_memory_update",
220
353
  ]);
221
354
  /**
222
- * 解析角色级记忆目录名。
223
- * 主流程:优先使用 AWS_ROLE_NAME,清理路径分隔符等非法字符,避免不同角色的长期 memory 混写。
355
+ * 从 startPath 向上逐级查找 .agentswork 目录。
356
+ * 主流程:从 startPath 开始检查是否存在 .agentswork -> 递归父目录直到根目录 -> 如果找到返回完整路径,否则在 startPath 创建。
224
357
  */
225
- static resolveRoleMemoryDirectoryName() {
226
- const roleName = String(process.env.AWS_ROLE_NAME || "").trim();
227
- const safeRoleName = roleName
358
+ static findAgentsworkDirSync(startPath) {
359
+ let current = path.resolve(startPath);
360
+ while (true) {
361
+ if (fs.existsSync(path.join(current, ".agentswork"))) {
362
+ return path.join(current, ".agentswork");
363
+ }
364
+ const parent = path.dirname(current);
365
+ if (parent === current)
366
+ break; // 到达根目录
367
+ current = parent;
368
+ }
369
+ return path.join(path.resolve(startPath), ".agentswork");
370
+ }
371
+ /**
372
+ * 清理角色名中的非法路径字符,返回安全的目录名。
373
+ */
374
+ static sanitizeRoleDirectoryName(roleName) {
375
+ const safeRoleName = String(roleName || "")
376
+ .trim()
228
377
  .split("")
229
378
  .map((character) => PATH_UNSAFE_CHARACTERS.has(character) || character.charCodeAt(0) < 32
230
379
  ? "_"
@@ -234,6 +383,21 @@ export class McpServer {
234
383
  .trim();
235
384
  return safeRoleName || "default-role";
236
385
  }
386
+ /**
387
+ * 解析角色级记忆目录名。
388
+ * 主流程:优先使用 AWS_ROLE_NAME,清理路径分隔符等非法字符,避免不同角色的长期 memory 混写。
389
+ */
390
+ static resolveRoleMemoryDirectoryName() {
391
+ return McpServer.sanitizeRoleDirectoryName(process.env.AWS_ROLE_NAME || "");
392
+ }
393
+ /**
394
+ * 根据 roleName 构建永久记忆存储路径。
395
+ */
396
+ static buildRoleMemoryStorePath(roleName) {
397
+ const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
398
+ const agentsworkDir = McpServer.findAgentsworkDirSync(workspacePath);
399
+ return path.join(agentsworkDir, "memory", "memory", McpServer.sanitizeRoleDirectoryName(roleName));
400
+ }
237
401
  /**
238
402
  * 解析记忆存储路径。
239
403
  * 主流程:task_list/recently_memory 走 server 数据库;长期 memory 默认写入项目 .agentswork/memory/memory/<roleName>/<domain>.json。
@@ -244,10 +408,11 @@ export class McpServer {
244
408
  return { instancePath: configuredPath, rolePath: configuredPath };
245
409
  }
246
410
  const workspacePath = process.env.AWS_WORKSPACE_PATH || process.cwd();
411
+ const agentsworkDir = McpServer.findAgentsworkDirSync(workspacePath);
247
412
  const instancePath = String(process.env.AWS_INSTANCE_MEMORY_STORE_PATH || "").trim() ||
248
- path.join(workspacePath, ".agentswork", "memory", "server-managed");
413
+ path.join(agentsworkDir, "memory", "server-managed");
249
414
  const rolePath = String(process.env.AWS_ROLE_MEMORY_STORE_PATH || "").trim() ||
250
- path.join(workspacePath, ".agentswork", "memory", "memory", McpServer.resolveRoleMemoryDirectoryName());
415
+ path.join(agentsworkDir, "memory", "memory", McpServer.resolveRoleMemoryDirectoryName());
251
416
  return { instancePath, rolePath };
252
417
  }
253
418
  getRequiredString(args, key) {
@@ -265,6 +430,10 @@ export class McpServer {
265
430
  const value = args[key];
266
431
  return typeof value === "number" ? value : undefined;
267
432
  }
433
+ getOptionalStringArray(args, key) {
434
+ const value = args[key];
435
+ return Array.isArray(value) ? value.map((v) => String(v)) : undefined;
436
+ }
268
437
  getOptionalMemoryType(args) {
269
438
  const value = args.memory_type;
270
439
  if (value === undefined) {
@@ -353,6 +522,7 @@ export class McpServer {
353
522
  ...roleStats.by_type.memory,
354
523
  count: roleStats.by_type.memory.count,
355
524
  },
525
+ session_context: instanceStats.by_type.session_context,
356
526
  },
357
527
  };
358
528
  }
@@ -372,6 +542,42 @@ export class McpServer {
372
542
  logger.warn("[McpServer] 上报合并 memory 统计失败,已保留本地记忆写入结果:", error);
373
543
  }
374
544
  }
545
+ /**
546
+ * 同步本地永久记忆到服务端。
547
+ * 主流程:读取本地 roleMemoryStore 中所有 memory 类型记录 -> 逐条写入服务端 memoryStore -> 已存在的记录静默跳过。
548
+ */
549
+ async syncLocalPermanentMemoryToServer() {
550
+ try {
551
+ const memories = await this.roleMemoryStore.listByType("memory");
552
+ if (memories.length === 0)
553
+ return;
554
+ let syncedCount = 0;
555
+ for (const memory of memories) {
556
+ try {
557
+ await this.memoryStore.createMemory({
558
+ memory_type: "memory",
559
+ uri: memory.uri,
560
+ title: memory.title,
561
+ content: memory.content,
562
+ keywords: memory.keywords,
563
+ bound_files: memory.bound_files,
564
+ });
565
+ syncedCount++;
566
+ }
567
+ catch (e) {
568
+ // If already exists on server, skip
569
+ const errMsg = e instanceof Error ? e.message : String(e);
570
+ if (!errMsg.includes("already exists")) {
571
+ logger.warn(`[McpServer] Failed to sync permanent memory ${memory.uri}:`, e);
572
+ }
573
+ }
574
+ }
575
+ logger.info(`[McpServer] Synced ${syncedCount}/${memories.length} permanent memories to server`);
576
+ }
577
+ catch (error) {
578
+ logger.error("[McpServer] Failed to sync permanent memories to server:", error);
579
+ }
580
+ }
375
581
  /**
376
582
  * 生成安全的工具状态上报参数。
377
583
  * 主流程:memory 工具只上报元数据,避免 content/query/old_string/new_string/append 离开本地记忆边界。
@@ -404,13 +610,42 @@ export class McpServer {
404
610
  * 主流程:校验必填字段 -> 交给 MemoryStore 规范化 URI 和类型 -> 返回不含 content 的写入结果。
405
611
  */
406
612
  async handleCreateMemory(args) {
613
+ const boundFiles = this.normalizeBoundFiles(this.getOptionalStringArray(args, "bound_files"));
614
+ // 验证绑定文件是否存在,避免创建无有效文件绑定的记忆
615
+ await this.validateBoundFilesExist(boundFiles);
616
+ // 创建时即计算绑定文件的 MD5 基线,避免首次文件变更检测产生假阳性
617
+ let fileMd5Map;
618
+ let lastCheckTime;
619
+ if (boundFiles && boundFiles.length > 0) {
620
+ fileMd5Map = {};
621
+ for (const f of boundFiles) {
622
+ const md5 = await this.calculateFileMd5(f);
623
+ if (md5)
624
+ fileMd5Map[f] = md5;
625
+ }
626
+ lastCheckTime = new Date().toISOString();
627
+ }
407
628
  const input = {
408
629
  memory_type: this.parseMemoryType(this.getRequiredString(args, "memory_type")),
409
630
  uri: this.getRequiredString(args, "uri"),
631
+ title: this.getOptionalString(args, "title"),
410
632
  content: this.getRequiredString(args, "content"),
633
+ keywords: this.getOptionalStringArray(args, "keywords"),
634
+ bound_files: boundFiles,
635
+ file_md5_map: fileMd5Map,
636
+ last_check_time: lastCheckTime,
411
637
  };
412
638
  const created = await this.getMemoryStoreForType(input.memory_type).createMemory(input);
413
639
  await this.reportCombinedMemoryStats();
640
+ // Dual-write permanent memory to server
641
+ if (input.memory_type === "memory") {
642
+ try {
643
+ await this.memoryStore.createMemory(input).catch(() => { });
644
+ }
645
+ catch (e) {
646
+ logger.warn("[McpServer] Failed to sync permanent memory to server:", e);
647
+ }
648
+ }
414
649
  return this.toMemoryWriteResult(created);
415
650
  }
416
651
  /**
@@ -444,16 +679,59 @@ export class McpServer {
444
679
  const input = {
445
680
  uri: this.getRequiredString(args, "uri"),
446
681
  memory_type: this.getOptionalMemoryType(args),
682
+ title: this.getOptionalString(args, "title"),
447
683
  old_string: this.getOptionalString(args, "old_string"),
448
684
  new_string: this.getOptionalString(args, "new_string"),
449
685
  append,
686
+ keywords: this.getOptionalStringArray(args, "keywords"),
687
+ bound_files: this.normalizeBoundFiles(this.getOptionalStringArray(args, "bound_files")),
450
688
  };
451
689
  const target = await this.findMemoryStoreForMutation(input);
690
+ // 读取当前记忆以获取 bound_files,用于 MD5 重算
691
+ const existingForMd5 = await target.store.readMemory({
692
+ uri: input.uri,
693
+ memory_type: target.memory_type ?? input.memory_type,
694
+ });
695
+ // 当显式传入新 bound_files 时验证文件是否存在
696
+ if (input.bound_files !== undefined) {
697
+ await this.validateBoundFilesExist(input.bound_files);
698
+ }
699
+ const effectiveBoundFiles = input.bound_files ?? existingForMd5?.bound_files;
700
+ // 重新计算绑定文件的 MD5 map,确保更新后 MD5 与文件实际内容一致
701
+ let fileMd5Map;
702
+ let lastCheckTime;
703
+ if (effectiveBoundFiles && effectiveBoundFiles.length > 0) {
704
+ fileMd5Map = {};
705
+ for (const f of effectiveBoundFiles) {
706
+ const md5 = await this.calculateFileMd5(f);
707
+ if (md5)
708
+ fileMd5Map[f] = md5;
709
+ }
710
+ lastCheckTime = new Date().toISOString();
711
+ }
452
712
  const updated = await target.store.updateMemory({
453
713
  ...input,
454
714
  memory_type: target.memory_type ?? input.memory_type,
715
+ file_md5_map: fileMd5Map,
716
+ last_check_time: lastCheckTime,
455
717
  });
456
718
  await this.reportCombinedMemoryStats();
719
+ // Dual-write permanent memory update to server(同步 MD5 map 避免服务端重复检测)
720
+ if (target.memory_type === "memory") {
721
+ try {
722
+ await this.memoryStore
723
+ .updateMemory({
724
+ ...input,
725
+ memory_type: "memory",
726
+ file_md5_map: fileMd5Map,
727
+ last_check_time: lastCheckTime,
728
+ })
729
+ .catch(() => { });
730
+ }
731
+ catch (e) {
732
+ logger.warn("[McpServer] Failed to sync permanent memory update to server:", e);
733
+ }
734
+ }
457
735
  return this.toMemoryWriteResult(updated);
458
736
  }
459
737
  /**
@@ -472,19 +750,57 @@ export class McpServer {
472
750
  });
473
751
  if (deleted) {
474
752
  await this.reportCombinedMemoryStats();
753
+ // Dual-write permanent memory delete to server
754
+ if (target.memory_type === "memory") {
755
+ try {
756
+ await this.memoryStore
757
+ .deleteMemory({ uri: input.uri, memory_type: "memory" })
758
+ .catch(() => { });
759
+ }
760
+ catch (e) {
761
+ logger.warn("[McpServer] Failed to sync permanent memory delete to server:", e);
762
+ }
763
+ }
475
764
  }
476
765
  return { deleted };
477
766
  }
478
767
  /**
479
- * 搜索本地记忆。
480
- * 主流程:校验 query -> MemoryStore uri/content 做大小写不敏感匹配和排序。
768
+ * 搜索本地记忆与子 Agent 记忆(父子 Agent 可见性)。
769
+ * 主流程:解析 scope -> own(默认)仅搜自己 -> children仅搜子Agent的file-scoped记忆 -> all合并。
770
+ * 子 Agent 判定规则:同项目且工作区路径为当前路径子目录。
481
771
  */
482
772
  async handleSearchMemory(args) {
773
+ const rawScope = this.getOptionalString(args, "scope");
774
+ const scope = rawScope === "children" || rawScope === "all" ? rawScope : "own";
483
775
  const input = {
484
776
  query: this.getRequiredString(args, "query"),
485
777
  limit: this.getOptionalNumber(args, "limit"),
486
778
  memory_type: this.getOptionalMemoryType(args),
779
+ keywords: this.getOptionalStringArray(args, "keywords"),
780
+ scope,
487
781
  };
782
+ // scope='children': 仅搜索子 Agent 的 file-scoped 记忆(通过 server 代理)
783
+ if (scope === "children") {
784
+ return await this.memoryStore.searchMemory(input);
785
+ }
786
+ // 搜索自己的记忆(scope='own' 默认行为)
787
+ const ownResult = await this.searchOwnMemories(input);
788
+ // scope='own': 仅返回自己的结果
789
+ if (scope === "own") {
790
+ return ownResult;
791
+ }
792
+ // scope='all': 合并自己的记忆与子 Agent 的 file-scoped 记忆
793
+ const childResult = await this.memoryStore.searchMemory({
794
+ ...input,
795
+ scope: "children",
796
+ });
797
+ return this.mergeSearchResults(ownResult, childResult, input.limit);
798
+ }
799
+ /**
800
+ * 搜索自己工作区的所有记忆。
801
+ * 具体逻辑:按 memory_type 分流 -> 无类型时合并 roleMemoryStore(永久)与 memoryStore(task_list/recently)。
802
+ */
803
+ async searchOwnMemories(input) {
488
804
  if (input.memory_type) {
489
805
  return await this.getMemoryStoreForType(input.memory_type).searchMemory(input);
490
806
  }
@@ -510,6 +826,26 @@ export class McpServer {
510
826
  .slice(0, limit);
511
827
  return { query: roleResult.query || instanceResult.query, results };
512
828
  }
829
+ /**
830
+ * 合并两组搜索结果,按 uri 去重、更新时间排序。
831
+ * 具体逻辑:own 结果优先 -> 补充 child 结果中 uri 不重复的 -> 排序 -> 截断到 limit。
832
+ */
833
+ mergeSearchResults(own, child, limit) {
834
+ const seen = new Set();
835
+ const maxResults = typeof limit === "number" && limit > 0 ? limit : 10;
836
+ const merged = [...own.results, ...child.results]
837
+ .filter((record) => {
838
+ const key = record.uri;
839
+ if (seen.has(key)) {
840
+ return false;
841
+ }
842
+ seen.add(key);
843
+ return true;
844
+ })
845
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
846
+ .slice(0, maxResults);
847
+ return { query: own.query || child.query, results: merged };
848
+ }
513
849
  /**
514
850
  * 列出短期类型记忆。
515
851
  * 主流程:只允许 task_list/recently_memory,由 MemoryStore 阻止长期 memory 的全量列出。
@@ -566,7 +902,10 @@ export class McpServer {
566
902
  const archived = await this.roleMemoryStore.createMemory({
567
903
  memory_type: "memory",
568
904
  uri: record.uri,
905
+ title: record.title,
569
906
  content: record.content,
907
+ keywords: record.keywords,
908
+ bound_files: record.bound_files,
570
909
  });
571
910
  await this.memoryStore.deleteMemory({
572
911
  uri: record.uri,
@@ -575,12 +914,231 @@ export class McpServer {
575
914
  await this.reportCombinedMemoryStats();
576
915
  return archived;
577
916
  }
917
+ /**
918
+ * 按文件查询绑定了该文件的 Memory
919
+ * Memory 增强功能 (v1.6.0)
920
+ */
921
+ /**
922
+ * 按文件路径查询 Memory(支持 scope 跨 Agent 搜索)。
923
+ * 主流程:解析scope -> own(默认)仅自己 -> children通过server按path_prefix匹配子Agent -> all合并。
924
+ * 对于永久记忆(type=memory)同时查询本地roleMemoryStore作为补充。
925
+ */
926
+ async handleListMemoriesByFile(args) {
927
+ const rawPath = this.getRequiredString(args, "file_path");
928
+ const normalizedPath = this.normalizeBoundFilePath(rawPath);
929
+ const memoryType = this.getOptionalMemoryType(args);
930
+ const rawScope = this.getOptionalString(args, "scope");
931
+ const scope = rawScope === "children" || rawScope === "all" ? rawScope : "own";
932
+ // scope='children': 仅通过 server 搜索子 Agent(带 path_prefix 自动匹配)
933
+ if (scope === "children") {
934
+ const childResult = await this.memoryStore.listByFile(normalizedPath, {
935
+ memory_type: memoryType,
936
+ scope: "children",
937
+ });
938
+ return {
939
+ file_path: normalizedPath,
940
+ count: childResult.length,
941
+ memories: childResult.map(m => this.toMemoryRecordWithMetadata(m)),
942
+ };
943
+ }
944
+ // 搜索自己的记忆
945
+ const ownMemories = await this.collectOwnMemoriesByFile(normalizedPath, memoryType);
946
+ if (scope === "own") {
947
+ return {
948
+ file_path: normalizedPath,
949
+ count: ownMemories.length,
950
+ memories: ownMemories.map(m => this.toMemoryRecordWithMetadata(m)),
951
+ };
952
+ }
953
+ // scope='all': 合并自己的 + 子 Agent 的
954
+ const childResult = await this.memoryStore.listByFile(normalizedPath, {
955
+ memory_type: memoryType,
956
+ scope: "children",
957
+ });
958
+ const seen = new Set(ownMemories.map(m => m.id));
959
+ const merged = [...ownMemories];
960
+ for (const m of childResult) {
961
+ if (!seen.has(m.id)) {
962
+ merged.push(m);
963
+ seen.add(m.id);
964
+ }
965
+ }
966
+ merged.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
967
+ return {
968
+ file_path: normalizedPath,
969
+ count: merged.length,
970
+ memories: merged.map(m => this.toMemoryRecordWithMetadata(m)),
971
+ };
972
+ }
973
+ /**
974
+ * 按文件路径搜索本 Agent 的记忆。
975
+ * 同时查询临时记忆(server)和永久记忆(local roleMemoryStore)。
976
+ */
977
+ async collectOwnMemoriesByFile(filePath, memoryType) {
978
+ const serverResult = await this.memoryStore.listByFile(filePath, {
979
+ memory_type: memoryType,
980
+ });
981
+ let localResult = [];
982
+ // 补充查询本地永久记忆(server 可能未同步)
983
+ if (!memoryType || memoryType === "memory") {
984
+ localResult = await this.roleMemoryStore.listByFile(filePath, "memory");
985
+ }
986
+ // 去重合并
987
+ const seen = new Set(serverResult.map(m => `${m.memory_type}:${m.uri}`));
988
+ const merged = [...serverResult];
989
+ for (const m of localResult) {
990
+ const key = `${m.memory_type}:${m.uri}`;
991
+ if (!seen.has(key)) {
992
+ merged.push(m);
993
+ seen.add(key);
994
+ }
995
+ }
996
+ return merged;
997
+ }
998
+ /**
999
+ * 放弃 Memory 更新状态
1000
+ * Memory 增强功能 (v1.6.0)
1001
+ */
1002
+ async handleAbandonMemoryUpdate(args) {
1003
+ const uri = this.getRequiredString(args, "uri");
1004
+ const memoryType = this.getOptionalMemoryType(args);
1005
+ const record = await this.memoryStore.readMemory({ uri, memory_type: memoryType });
1006
+ if (!record) {
1007
+ throw new Error(`Memory not found: ${uri}`);
1008
+ }
1009
+ // 清除 updating 状态
1010
+ await this.memoryStore.updateMemory({
1011
+ uri: record.uri,
1012
+ memory_type: record.memory_type,
1013
+ });
1014
+ return {
1015
+ success: true,
1016
+ uri: record.uri,
1017
+ message: "Memory 更新状态已清除",
1018
+ };
1019
+ }
1020
+ // ============ 上下文压缩工具处理 (v1.8.0) ============
1021
+ /**
1022
+ * 获取当前会话的上下文统计。
1023
+ * 主流程:从 profile 获取模型名称 -> 通过模型名查找上下文窗口 -> 调用 ContextManager 获取统计 -> 返回。
1024
+ */
1025
+ async handleGetContextStats(args) {
1026
+ const profile = await this.agentClient.getProfile().catch(() => null);
1027
+ const sessionId = args.sessionId ?? profile?.runtimeSessionId ?? "default";
1028
+ const modelName = profile?.prompt ? this.guessModelName(profile) : "unknown";
1029
+ const modelInfo = {
1030
+ provider: modelName.split("/")[0] || "unknown",
1031
+ model: modelName,
1032
+ contextLimit: 0, // ContextManager 内部会通过 lookupModelLimits 查找
1033
+ };
1034
+ const stats = await this.contextManager.getStats(sessionId, modelInfo);
1035
+ // 如果 contextLimit 仍为 0,用查找表的默认值
1036
+ if (stats.contextLimit === 0) {
1037
+ const limits = this.contextManager.lookupModelLimits(modelName);
1038
+ return { ...stats, contextLimit: limits.contextLimit, remaining: limits.contextLimit - stats.totalUsed, usagePercent: Math.round((stats.totalUsed / limits.contextLimit) * 100) };
1039
+ }
1040
+ return stats;
1041
+ }
1042
+ /**
1043
+ * 从 profile 猜测当前使用的模型名称。
1044
+ * 主流程:读取 prompt 中的模型信息 -> 回退到 runtimeCliCommand 中的提示 -> 最终回退到环境变量或默认值。
1045
+ */
1046
+ guessModelName(profile) {
1047
+ // 尝试从 prompt 中提取模型名(prompt 可能包含 @model 标记)
1048
+ const promptMatch = profile.prompt?.match(/@model\s+(\S+)/i);
1049
+ if (promptMatch?.[1])
1050
+ return promptMatch[1];
1051
+ // 回退到 CLI 命令中的线索
1052
+ if (profile.runtimeMode)
1053
+ return profile.runtimeMode;
1054
+ // 默认值
1055
+ return process.env.AWS_DEFAULT_MODEL || "gpt-4o";
1056
+ }
1057
+ /**
1058
+ * 处理上下文压缩请求。
1059
+ * 主流程:解析策略 -> 获取消息历史 -> 执行压缩 -> 保存结果 -> 返回。
1060
+ */
1061
+ async handleCompactContext(args) {
1062
+ const profile = await this.agentClient.getProfile().catch(() => null);
1063
+ const sessionId = profile?.runtimeSessionId ?? "default";
1064
+ const strategy = args.strategy ?? "auto";
1065
+ const keepLastTurns = args.keepLastTurns ?? 3;
1066
+ // 获取消息历史
1067
+ const messages = await this.fetchRecentMessages();
1068
+ switch (strategy) {
1069
+ case "prune": {
1070
+ const result = this.contextManager.prune(messages, keepLastTurns);
1071
+ await this.contextManager.saveCompactionResult(sessionId, result);
1072
+ return result;
1073
+ }
1074
+ case "summarize": {
1075
+ // summarize 需要 Agent 自己调用 LLM 来完成
1076
+ // 这里先执行 prune 释放部分空间,然后返回需要 Agent 进行 summarize
1077
+ const pruneResult = this.contextManager.prune(messages, keepLastTurns);
1078
+ await this.contextManager.saveCompactionResult(sessionId, pruneResult);
1079
+ return {
1080
+ ...pruneResult,
1081
+ strategy: "summarize",
1082
+ summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
1083
+ };
1084
+ }
1085
+ case "auto":
1086
+ default: {
1087
+ // 先尝试 prune
1088
+ const pruneResult = this.contextManager.prune(messages, keepLastTurns);
1089
+ if (pruneResult.freedTokens >= 20_000) {
1090
+ await this.contextManager.saveCompactionResult(sessionId, pruneResult);
1091
+ return pruneResult;
1092
+ }
1093
+ // prune 释放不够,回退到 summarize
1094
+ return {
1095
+ ...pruneResult,
1096
+ strategy: "summarize",
1097
+ summary: await this.contextManager.getCompactionSummary(sessionId) ?? undefined,
1098
+ };
1099
+ }
1100
+ }
1101
+ }
1102
+ /**
1103
+ * 获取最近的消息历史用于压缩。
1104
+ * 主流程:获取未读消息 + 历史消息 -> 合并排序 -> 返回。
1105
+ */
1106
+ async fetchRecentMessages() {
1107
+ const [dmHistory, groupHistory] = await Promise.all([
1108
+ this.agentClient.getDmHistory(undefined, 200).catch(() => ({ messages: [], count: 0 })),
1109
+ this.agentClient.getGroupHistory(200).catch(() => ({ messages: [], count: 0 })),
1110
+ ]);
1111
+ const messages = [
1112
+ ...dmHistory.messages,
1113
+ ...groupHistory.messages,
1114
+ ];
1115
+ messages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1116
+ return messages;
1117
+ }
578
1118
  async handleSendGroup(args) {
579
1119
  if (!args.content || typeof args.content !== "string") {
580
1120
  throw new Error("content is required");
581
1121
  }
1122
+ // 记录 token 消耗 (v1.8.0)
1123
+ this.recordMessageTokenUsage(args.content).catch((err) => logger.warn("[McpServer] Failed to record token usage", err));
582
1124
  return await this.agentClient.sendGroupMessage(args.content, args.projectName, args.roomId);
583
1125
  }
1126
+ /**
1127
+ * 记录消息发送产生的 token 消耗。
1128
+ * 主流程:估算内容 token -> 获取会话 ID -> 记录到 ContextManager。
1129
+ */
1130
+ async recordMessageTokenUsage(content) {
1131
+ try {
1132
+ const profile = await this.agentClient.getProfile().catch(() => null);
1133
+ if (!profile?.runtimeSessionId)
1134
+ return;
1135
+ const estimatedTokens = this.contextManager.estimateTokens(content);
1136
+ await this.contextManager.recordUsage(profile.runtimeSessionId, { input: 0, output: estimatedTokens });
1137
+ }
1138
+ catch {
1139
+ // 非关键功能,静默失败
1140
+ }
1141
+ }
584
1142
  async handleHireColleague(args) {
585
1143
  if (!args.workspacePath || typeof args.workspacePath !== "string") {
586
1144
  throw new Error("workspacePath is required");
@@ -605,6 +1163,8 @@ export class McpServer {
605
1163
  if (!args.content || typeof args.content !== "string") {
606
1164
  throw new Error("content is required");
607
1165
  }
1166
+ // 记录 token 消耗 (v1.8.0)
1167
+ this.recordMessageTokenUsage(args.content).catch((err) => logger.warn("[McpServer] Failed to record token usage", err));
608
1168
  return await this.agentClient.sendDirectMessage(args.targetId, args.content, args.waitReply, args.replyToCallId, args.timeoutMs);
609
1169
  }
610
1170
  async handleCreateTaskInstanceComment(args) {
@@ -620,6 +1180,20 @@ export class McpServer {
620
1180
  blockIfEmpty: false,
621
1181
  includePipelineTasks: true,
622
1182
  });
1183
+ // Memory 文件变更检测通知(优先级最高)
1184
+ // 后台定时器启用时由定时器负责推送,poll_message 不再重复检查
1185
+ if (!this.memoryFileCheckInterval) {
1186
+ const fileChangeNotification = await this.buildMemoryFileChangeNotification();
1187
+ if (fileChangeNotification) {
1188
+ const messages = this.messageBuffer.get();
1189
+ messages.directMessages.unshift(fileChangeNotification);
1190
+ return {
1191
+ directMessages: messages.directMessages,
1192
+ groupMessages: messages.groupMessages,
1193
+ fromBuffer: true,
1194
+ };
1195
+ }
1196
+ }
623
1197
  const immediateMemoryNotification = await this.buildMemoryCleanupNotification();
624
1198
  if (immediateMemoryNotification) {
625
1199
  const messages = this.messageBuffer.get();
@@ -632,16 +1206,84 @@ export class McpServer {
632
1206
  }
633
1207
  await this.hydrateUnreadMessagesUntilAvailable();
634
1208
  const messages = this.messageBuffer.get();
1209
+ // 后台定时器启用时跳过,避免与定时器推送重复
1210
+ if (!this.memoryFileCheckInterval) {
1211
+ const fileChangeNotification2 = await this.buildMemoryFileChangeNotification();
1212
+ if (fileChangeNotification2) {
1213
+ messages.directMessages.unshift(fileChangeNotification2);
1214
+ }
1215
+ }
635
1216
  const memoryNotification = await this.buildMemoryCleanupNotification();
636
1217
  if (memoryNotification) {
637
1218
  messages.directMessages.unshift(memoryNotification);
638
1219
  }
1220
+ // 上下文溢出检测 (v1.8.0): 在消息返回前检查上下文是否接近溢出
1221
+ await this.checkContextOverflow(messages);
639
1222
  return {
640
1223
  directMessages: messages.directMessages,
641
1224
  groupMessages: messages.groupMessages,
642
1225
  fromBuffer: true,
643
1226
  };
644
1227
  }
1228
+ /**
1229
+ * 检查当前会话的上下文是否接近溢出,如果是则添加溢出警告消息。
1230
+ * 主流程:获取 sessionId -> 累计消息 token -> 检查 ContextManager.isOverflow -> 若溢出则插入警告。
1231
+ */
1232
+ async checkContextOverflow(messages) {
1233
+ try {
1234
+ const profile = await this.agentClient.getProfile().catch(() => null);
1235
+ const sessionId = profile?.runtimeSessionId ?? "default";
1236
+ // 估算本次返回消息的 token 并记录
1237
+ const allMessages = [...messages.directMessages, ...messages.groupMessages];
1238
+ for (const msg of allMessages) {
1239
+ const tokenEstimate = this.contextManager.estimateTokens(msg.content);
1240
+ const tokens = msg.tokens ?? { input: 0, output: 0 };
1241
+ tokens.input += tokenEstimate;
1242
+ msg.tokens = tokens;
1243
+ }
1244
+ // 记录累计用量(取最后一条消息的累计作为参考)
1245
+ if (allMessages.length > 0) {
1246
+ const lastMsg = allMessages[allMessages.length - 1];
1247
+ await this.contextManager.recordUsage(sessionId, lastMsg.tokens ?? { input: 0, output: 0 });
1248
+ }
1249
+ // 检查是否溢出
1250
+ const modelName = profile ? this.guessModelName(profile) : "unknown";
1251
+ const limits = this.contextManager.lookupModelLimits(modelName);
1252
+ const modelInfo = {
1253
+ provider: modelName.split("/")[0] || "unknown",
1254
+ model: modelName,
1255
+ contextLimit: limits.contextLimit,
1256
+ maxOutput: limits.maxOutput,
1257
+ };
1258
+ const overflow = await this.contextManager.isOverflow(sessionId, modelInfo);
1259
+ if (overflow) {
1260
+ const stats = await this.contextManager.getStats(sessionId, modelInfo);
1261
+ // 插入溢出警告作为首条私信
1262
+ messages.directMessages.unshift({
1263
+ msgId: `context-overflow-${Date.now()}`,
1264
+ senderId: "system",
1265
+ senderName: "System",
1266
+ content: [
1267
+ "⚠️ **上下文溢出警告**",
1268
+ "",
1269
+ `当前会话已使用约 **${stats.usagePercent}%**(${Math.round(stats.totalUsed / 1000)}K / ${Math.round(stats.contextLimit / 1000)}K token)的上下文窗口。`,
1270
+ `剩余空间约 **${Math.round(stats.remaining / 1000)}K token**。`,
1271
+ "",
1272
+ "建议调用 `compact_context` 释放空间:",
1273
+ "1. `compact_context({ strategy: \"prune\" })` — 快速裁剪旧工具输出",
1274
+ "2. `compact_context({ strategy: \"summarize\" })` — 摘要化早期对话(效果更好)",
1275
+ ].join("\n"),
1276
+ requireReply: false,
1277
+ timestamp: new Date().toISOString(),
1278
+ compacted: false,
1279
+ });
1280
+ }
1281
+ }
1282
+ catch (error) {
1283
+ // 溢出检测是辅助功能,不应中断主流程
1284
+ logger.warn("[McpServer] Context overflow check failed", error);
1285
+ }
1286
+ }
645
1287
  /**
646
1288
  * 持续补拉未读消息直到有消息可返回。
647
1289
  * 主流程:用有限后端长轮询分片阻塞补拉 -> 空返回时短暂退避 -> 继续等待,避免 poll_message 空轮询唤醒模型。
@@ -743,6 +1385,249 @@ export class McpServer {
743
1385
  return null;
744
1386
  }
745
1387
  }
1388
+ /**
1389
+ * 构建 Memory 文件变更检测通知。
1390
+ * Memory 增强功能 (v1.6.0)
1391
+ *
1392
+ * 检测流程:
1393
+ * 1. 检查本 Agent 标记为 updating 但未处理的 Memory
1394
+ * 2. 检查短期记忆、近期记忆绑定的文件是否变更
1395
+ * 3. 如果没有变更,检查永久记忆的文件变更,标记最多 5 个为 updating
1396
+ */
1397
+ async buildMemoryFileChangeNotification() {
1398
+ try {
1399
+ const agentId = this.getCurrentAgentId();
1400
+ if (!agentId) {
1401
+ return null;
1402
+ }
1403
+ // 步骤1: 检查是否有 pending 的 updating memories(角色本地永久记忆),
1404
+ // 同时检测绑定文件是否再次变更(用户可能在 Agent 处理前又改了文件)
1405
+ const permanentList = await this.roleMemoryStore.listByType('memory');
1406
+ const updatingMemories = permanentList.filter(m => m.update_status === 'updating' && m.updating_agent_id === agentId);
1407
+ if (updatingMemories.length > 0) {
1408
+ // 对 updating 记忆也检测文件是否再次变更
1409
+ const updatingChanges = await this.checkMemoriesFileChanges(updatingMemories);
1410
+ if (updatingChanges.length > 0) {
1411
+ // 有新的文件变更,更新 MD5 map 持久化,发出新变更通知
1412
+ for (const change of updatingChanges) {
1413
+ const memory = updatingMemories.find(m => m.id === change.memoryId);
1414
+ if (memory) {
1415
+ await this.roleMemoryStore.updateMemory({
1416
+ uri: memory.uri,
1417
+ memory_type: 'memory',
1418
+ update_status: 'updating',
1419
+ updating_agent_id: agentId,
1420
+ file_md5_map: memory.file_md5_map,
1421
+ last_check_time: memory.last_check_time,
1422
+ });
1423
+ }
1424
+ }
1425
+ const selectedCount = Math.min(updatingChanges.length, 5);
1426
+ const changeDetails = updatingChanges
1427
+ .slice(0, 5)
1428
+ .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
1429
+ .join('\n');
1430
+ return {
1431
+ msgId: `memory-file-change-${Date.now()}`,
1432
+ senderId: "MCP-System",
1433
+ senderName: "系统",
1434
+ receiverId: agentId,
1435
+ content: `检测到以下 ${selectedCount} 个处于更新中的 Memory 绑定的文件再次变更,已更新 MD5 记录:\n\n${changeDetails}\n\n请调用 read_memory 查看最新内容并调用 update_memory 更新。`,
1436
+ requireReply: false,
1437
+ prompt: "这是一条文件变更提醒,您之前标记为更新中的记忆绑定的文件又有新变更,请处理。",
1438
+ timestamp: new Date().toISOString(),
1439
+ };
1440
+ }
1441
+ const memoryList = updatingMemories
1442
+ .slice(0, 5)
1443
+ .map(m => `- ${m.uri} (文件: ${m.bound_files?.join(', ') || '未知'})`)
1444
+ .join('\n');
1445
+ return {
1446
+ msgId: `memory-update-pending-${Date.now()}`,
1447
+ senderId: "MCP-System",
1448
+ senderName: "系统",
1449
+ receiverId: agentId,
1450
+ content: `您有以下 Memory 正处于更新中状态但尚未完成更新:\n\n${memoryList}\n\n请继续完成更新工作。`,
1451
+ requireReply: false,
1452
+ prompt: "这是一条更新提醒,您之前已标记这些记忆为更新中,请继续处理。",
1453
+ timestamp: new Date().toISOString(),
1454
+ };
1455
+ }
1456
+ // 步骤2: 检查短期记忆和近期记忆(服务端存储)
1457
+ const recentlyMemories = await this.memoryStore.listByType('recently_memory');
1458
+ const taskListMemories = await this.memoryStore.listByType('task_list');
1459
+ const listableMemories = [...recentlyMemories, ...taskListMemories];
1460
+ // 用本地 MD5 缓存补全 listable 记忆(服务端可能不持久化 file_md5_map 字段)
1461
+ // 确保 checkMemoriesFileChanges 有正确的旧 MD5 做比对而非始终以空字典开始
1462
+ for (const m of listableMemories) {
1463
+ if (!m.file_md5_map) {
1464
+ const cached = this.listableMd5Cache.get(m.id);
1465
+ if (cached) {
1466
+ m.file_md5_map = cached;
1467
+ }
1468
+ }
1469
+ }
1470
+ const listableChanges = await this.checkMemoriesFileChanges(listableMemories);
1471
+ // 使用复合 key(memoryId + 当前各绑定文件 MD5)过滤已通知变更,
1472
+ // 同一条记忆文件再次变更(MD5 变化)时仍能触发新通知
1473
+ const newListableChanges = listableChanges.filter(c => {
1474
+ const memory = listableMemories.find(m => m.id === c.memoryId);
1475
+ const md5Map = memory?.file_md5_map;
1476
+ const changeKey = this.buildMemoryChangeNotificationKey(c.memoryId, md5Map);
1477
+ return this.notifiedFileChangeMemoryIds.get(c.memoryId) !== changeKey;
1478
+ });
1479
+ if (newListableChanges.length > 0) {
1480
+ // 记录当前状态的复合键,下次对比时若 MD5 变化则重新通知
1481
+ // 同时将 MD5 map 写入本地缓存,弥补服务端不持久化 file_md5_map 的问题
1482
+ for (const c of newListableChanges) {
1483
+ const memory = listableMemories.find(m => m.id === c.memoryId);
1484
+ const md5Map = memory?.file_md5_map;
1485
+ this.notifiedFileChangeMemoryIds.set(c.memoryId, this.buildMemoryChangeNotificationKey(c.memoryId, md5Map));
1486
+ if (memory?.file_md5_map) {
1487
+ this.listableMd5Cache.set(c.memoryId, memory.file_md5_map);
1488
+ }
1489
+ }
1490
+ const changeDetails = newListableChanges
1491
+ .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
1492
+ .join('\n');
1493
+ return {
1494
+ msgId: `memory-file-change-${Date.now()}`,
1495
+ senderId: "MCP-System",
1496
+ senderName: "系统",
1497
+ receiverId: agentId,
1498
+ content: `检测到以下 Memory 绑定的文件已变更,请及时更新:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。`,
1499
+ requireReply: false,
1500
+ prompt: "这是一条文件变更提醒,自动生成,无需持久化。",
1501
+ timestamp: new Date().toISOString(),
1502
+ };
1503
+ }
1504
+ // 步骤3: 检查永久记忆(角色本地存储)
1505
+ const normalPermanentMemories = permanentList.filter(m => m.bound_files && m.bound_files.length > 0 &&
1506
+ (m.update_status === 'normal' || !m.update_status));
1507
+ const permanentChanges = await this.checkMemoriesFileChanges(normalPermanentMemories);
1508
+ if (permanentChanges.length > 0) {
1509
+ // 最多选取 5 个
1510
+ const selectedChanges = permanentChanges.slice(0, 5);
1511
+ // 标记为 updating 状态,同时持久化当前 MD5 map,
1512
+ // 确保后续检测能正确对比文件变更,而非每次都视为"已变更"
1513
+ for (const change of selectedChanges) {
1514
+ const memory = permanentList.find(m => m.id === change.memoryId);
1515
+ if (memory) {
1516
+ await this.roleMemoryStore.updateMemory({
1517
+ uri: memory.uri,
1518
+ memory_type: 'memory',
1519
+ update_status: 'updating',
1520
+ updating_agent_id: agentId,
1521
+ // checkMemoriesFileChanges 已更新 in-memory 对象中的 MD5 map
1522
+ file_md5_map: memory.file_md5_map,
1523
+ last_check_time: memory.last_check_time,
1524
+ });
1525
+ }
1526
+ }
1527
+ const changeDetails = selectedChanges
1528
+ .map(change => `- ${change.memoryUri} (文件: ${change.changedFiles.join(', ')})`)
1529
+ .join('\n');
1530
+ return {
1531
+ msgId: `memory-file-change-${Date.now()}`,
1532
+ senderId: "MCP-System",
1533
+ senderName: "系统",
1534
+ receiverId: agentId,
1535
+ content: `检测到以下 ${selectedChanges.length} 个永久记忆绑定的文件已变更,已为您标记为更新中:\n\n${changeDetails}\n\n请调用 read_memory 查看内容并调用 update_memory 更新。更新完成后状态将自动恢复为正常。`,
1536
+ requireReply: false,
1537
+ prompt: "这是一条文件变更提醒,已自动标记更新状态,请处理。",
1538
+ timestamp: new Date().toISOString(),
1539
+ };
1540
+ }
1541
+ return null;
1542
+ }
1543
+ catch (error) {
1544
+ logger.warn("[McpServer] 构建 Memory 文件变更检测通知失败:", error);
1545
+ return null;
1546
+ }
1547
+ }
1548
+ /**
1549
+ * 检查一组 Memory 的文件变更
1550
+ * Memory 增强功能 (v1.6.0)
1551
+ */
1552
+ async checkMemoriesFileChanges(memories) {
1553
+ const changes = [];
1554
+ for (const memory of memories) {
1555
+ if (!memory.bound_files || memory.bound_files.length === 0) {
1556
+ continue;
1557
+ }
1558
+ try {
1559
+ const changedFiles = [];
1560
+ const newMd5Map = {};
1561
+ const oldMd5Map = memory.file_md5_map || {};
1562
+ for (const filePath of memory.bound_files) {
1563
+ try {
1564
+ const currentMd5 = await this.calculateFileMd5(filePath);
1565
+ if (currentMd5 === null) {
1566
+ // 文件不存在或无法读取
1567
+ changedFiles.push(`${filePath} (文件不存在或无法访问)`);
1568
+ continue;
1569
+ }
1570
+ newMd5Map[filePath] = currentMd5;
1571
+ const oldMd5 = oldMd5Map[filePath];
1572
+ if (!oldMd5 || oldMd5 !== currentMd5) {
1573
+ changedFiles.push(filePath);
1574
+ }
1575
+ }
1576
+ catch (error) {
1577
+ logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
1578
+ changedFiles.push(`${filePath} (MD5计算失败)`);
1579
+ }
1580
+ }
1581
+ if (changedFiles.length > 0) {
1582
+ changes.push({
1583
+ memoryId: memory.id,
1584
+ memoryUri: memory.uri,
1585
+ changedFiles,
1586
+ });
1587
+ // 更新 MD5 映射
1588
+ memory.file_md5_map = newMd5Map;
1589
+ memory.last_check_time = new Date().toISOString();
1590
+ }
1591
+ }
1592
+ catch (error) {
1593
+ logger.warn(`[McpServer] 检查 Memory ${memory.id} 文件变更失败:`, error);
1594
+ }
1595
+ }
1596
+ return changes;
1597
+ }
1598
+ /**
1599
+ * 计算文件 MD5
1600
+ * Memory 增强功能 (v1.6.0)
1601
+ */
1602
+ async calculateFileMd5(filePath) {
1603
+ try {
1604
+ const { createReadStream } = await import('node:fs');
1605
+ const { createHash } = await import('node:crypto');
1606
+ return new Promise((resolve) => {
1607
+ const hash = createHash('md5');
1608
+ const stream = createReadStream(filePath);
1609
+ stream.on('data', (chunk) => hash.update(chunk));
1610
+ stream.on('end', () => resolve(hash.digest('hex')));
1611
+ stream.on('error', () => resolve(null));
1612
+ });
1613
+ }
1614
+ catch (error) {
1615
+ logger.info(`[McpServer] 计算文件 MD5 失败: ${filePath}`, error);
1616
+ return null;
1617
+ }
1618
+ }
1619
+ /**
1620
+ * 构建 Memory 文件变更通知的复合去重键。
1621
+ * 基于 memoryId + 当前各绑定文件的 MD5 生成,当任意绑定文件内容变化时复合键也随之变化,
1622
+ * 从而确保真实文件变更不会因简单 memoryId 去重而静默吞掉。
1623
+ */
1624
+ buildMemoryChangeNotificationKey(memoryId, md5Map) {
1625
+ if (!md5Map || Object.keys(md5Map).length === 0) {
1626
+ return memoryId;
1627
+ }
1628
+ const sorted = Object.entries(md5Map).sort(([a], [b]) => a.localeCompare(b));
1629
+ return `${memoryId}:${sorted.map(([k, v]) => `${k}=${v}`).join('|')}`;
1630
+ }
746
1631
  async handleGetMessage() {
747
1632
  await this.hydrateUnreadMessagesIfNeeded();
748
1633
  const messages = this.messageBuffer.get();
@@ -949,8 +1834,54 @@ export class McpServer {
949
1834
  }
950
1835
  // 后台注册:不阻塞 MCP 工具暴露,Agent 可立即接收工具调用
951
1836
  this.autoRegisterInBackground();
1837
+ // 启动后台 Memory 文件变更检测
1838
+ this.startMemoryFileWatcher();
952
1839
  logger.info("[McpServer] MCP Server 已就绪,工具已可用");
953
1840
  }
1841
+ /**
1842
+ * 停止 McpServer,清理后台定时器。
1843
+ * 主流程:清理文件变更检测定时器 -> 停止 Agent 客户端消息循环。
1844
+ */
1845
+ stop() {
1846
+ this.stopMemoryFileWatcher();
1847
+ this.agentClient.stop();
1848
+ logger.info("[McpServer] MCP Server 已停止");
1849
+ }
1850
+ /**
1851
+ * 启动后台 Memory 文件变更检测定时器。
1852
+ * 主流程:读取 AWS_MEMORY_FILE_CHECK_INTERVAL_MS -> 启动 setInterval -> 每次触发时检查绑定文件 MD5 变更 -> 有变更时直接推入消息缓冲区。
1853
+ * 后台定时器启用后,poll_message 不再重复检查,避免重复推送通知。
1854
+ */
1855
+ startMemoryFileWatcher() {
1856
+ const intervalMs = this.parsePositiveIntegerEnv("AWS_MEMORY_FILE_CHECK_INTERVAL_MS", DEFAULT_MEMORY_FILE_CHECK_INTERVAL_MS);
1857
+ if (intervalMs <= 0) {
1858
+ logger.info("[McpServer] 后台文件变更检测已禁用(AWS_MEMORY_FILE_CHECK_INTERVAL_MS=0),将回退到 poll_message 时检测");
1859
+ return;
1860
+ }
1861
+ this.memoryFileCheckInterval = setInterval(async () => {
1862
+ try {
1863
+ const notification = await this.buildMemoryFileChangeNotification();
1864
+ if (notification) {
1865
+ this.messageBuffer.push(notification);
1866
+ logger.info("[McpServer] 后台文件变更检测: 已推送 Memory 变更通知到消息缓冲区");
1867
+ }
1868
+ }
1869
+ catch (error) {
1870
+ logger.warn("[McpServer] 后台文件变更检测失败:", error);
1871
+ }
1872
+ }, intervalMs);
1873
+ logger.info(`[McpServer] 后台文件变更检测已启动,间隔: ${intervalMs}ms(可通过 AWS_MEMORY_FILE_CHECK_INTERVAL_MS 环境变量调整,设为 0 禁用)`);
1874
+ }
1875
+ /**
1876
+ * 停止后台 Memory 文件变更检测定时器。
1877
+ */
1878
+ stopMemoryFileWatcher() {
1879
+ if (this.memoryFileCheckInterval) {
1880
+ clearInterval(this.memoryFileCheckInterval);
1881
+ this.memoryFileCheckInterval = null;
1882
+ logger.info("[McpServer] 后台文件变更检测已停止");
1883
+ }
1884
+ }
954
1885
  async autoRegister() {
955
1886
  const agentConfig = this.agentClient.getAgentConfig();
956
1887
  logger.info(`[McpServer] 角色: "${agentConfig.roleName}"`);
@@ -971,6 +1902,11 @@ export class McpServer {
971
1902
  statusReporter.reportWaitingInput();
972
1903
  }
973
1904
  await this.reportCombinedMemoryStats();
1905
+ // 如果是新创建的角色,将本地永久记忆同步到服务端
1906
+ if (result.isExisting === false) {
1907
+ logger.info("[McpServer] 新角色创建,同步本地永久记忆到服务端...");
1908
+ await this.syncLocalPermanentMemoryToServer();
1909
+ }
974
1910
  return;
975
1911
  }
976
1912
  catch (error) {
@@ -1026,6 +1962,7 @@ export class McpServer {
1026
1962
  // 收到 Profile 信息后,存储到消息缓冲区
1027
1963
  logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
1028
1964
  this.messageBuffer.setProfileMessage(profile);
1965
+ this.syncRoleMemoryStore(profile);
1029
1966
  },
1030
1967
  });
1031
1968
  return;
@@ -1036,6 +1973,7 @@ export class McpServer {
1036
1973
  // 收到 Profile 信息后,存储到消息缓冲区
1037
1974
  logger.info(`[McpServer] profileHandler 被调用,displayName: ${profile.displayName}`);
1038
1975
  this.messageBuffer.setProfileMessage(profile);
1976
+ this.syncRoleMemoryStore(profile);
1039
1977
  },
1040
1978
  });
1041
1979
  }