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