evolcore 0.0.29 → 0.0.30
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/CHANGELOG.md +7 -0
- package/dist/agents/codex-runner.js +61 -14
- package/dist/aun/aid/agentmd.js +224 -19
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +70 -4
- package/dist/aun/group/index.js +1 -0
- package/dist/{core → aun}/group/invite-service.js +4 -4
- package/dist/channels/daemon.js +1 -1
- package/dist/cli/agent-command.js +7 -7
- package/dist/cli/aun-commands.js +97 -37
- package/dist/cli/data-command.js +2 -1
- package/dist/cli/managed-command-guard.js +2 -2
- package/dist/cli/model.js +1 -1
- package/dist/cli/response.js +1 -1
- package/dist/cli/trigger-command.js +52 -29
- package/dist/config/boot-log.js +95 -1
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/peer-role-resolver.js +21 -0
- package/dist/core/agent-ipc-registration.js +409 -0
- package/dist/core/agent-runtime.js +435 -0
- package/dist/core/auth/delegation-authorization.js +58 -0
- package/dist/core/auth/operation-catalog.js +8 -0
- package/dist/core/bootstrap-service.js +65 -1
- package/dist/core/channel-runtime.js +435 -0
- package/dist/core/command/command-handler.js +4 -1
- package/dist/core/daemon-bootstrap.js +151 -0
- package/dist/core/daemon-control.js +205 -0
- package/dist/core/daemon-lifecycle.js +225 -0
- package/dist/core/data-migration.js +4 -1
- package/dist/core/handoff/ipc-registration.js +156 -0
- package/dist/core/ipc-registration.js +457 -0
- package/dist/core/memory/consolidation-trigger.js +8 -0
- package/dist/core/memory/consolidation.js +838 -0
- package/dist/core/message/aun-ipc-sender.js +226 -0
- package/dist/core/message/outbound.js +165 -0
- package/dist/core/permission/approval-gateway.js +13 -0
- package/dist/core/permission/ec-command-parser.js +80 -35
- package/dist/core/permission/sandbox-runtime.js +23 -2
- package/dist/core/permission/tool-error-code.js +1 -0
- package/dist/core/permission/tool-policy.js +61 -24
- package/dist/core/session/session-idle-monitor.js +17 -0
- package/dist/index.js +352 -4841
- package/dist/ipc.js +2 -1
- package/dist/stats/db.js +24 -0
- package/dist/stats/index.js +1 -1
- package/dist/stats/writer.js +25 -1
- package/dist/trigger/application-service.js +157 -0
- package/dist/trigger/authorization.js +371 -0
- package/dist/trigger/ipc-service.js +406 -0
- package/dist/trigger/local-files.js +133 -0
- package/dist/trigger/manager.js +235 -22
- package/dist/trigger/scheduler.js +30 -5
- package/dist/trigger/script-executor.js +176 -39
- package/dist/trigger/script-sandbox.js +458 -0
- package/dist/trigger/system-definitions.js +188 -0
- package/dist/trigger/validation.js +0 -3
- package/dist/utils/aid-bind.js +1 -1
- package/dist/utils/evolcore-version.js +74 -0
- package/dist/utils/logger.js +16 -0
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/model.md +3 -2
- package/kits/docs/evolcore/rpc.md +4 -0
- package/kits/docs/evolcore/trigger.md +15 -1
- package/kits/schemas/relation-config.schema.9.json +1 -1
- package/kits/templates/system-fragments/commands.md +0 -1
- package/package.json +1 -1
- package/dist/core/memory/launch-consolidation.js +0 -395
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consolidation.ts — 记忆整理生命周期
|
|
3
|
+
*
|
|
4
|
+
* 两个入口共用这一处实现:
|
|
5
|
+
* - ipc.ts:手动 `ec memory consolidate --mode X`
|
|
6
|
+
* - index.ts:定时检查(五类候选)、compress / session-new 自动触发
|
|
7
|
+
*
|
|
8
|
+
* 流程:请求入队 → 序列化上下文 → 创建整理会话 → 完成或回滚 → 反馈投递。
|
|
9
|
+
* `execution.memoryMode` 会经 session.metadata 透传到渲染层与 ipc 的权限校验。
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { agentDir, resolveRoot } from '../../paths.js';
|
|
14
|
+
import { serializeConsolidationContext } from './consolidation-context.js';
|
|
15
|
+
import { normalizeTriggerDefinition } from '../../trigger/validation.js';
|
|
16
|
+
import { formatChannelKey } from '../channel-loader.js';
|
|
17
|
+
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
|
+
import { logger } from '../../utils/logger.js';
|
|
19
|
+
import { appendOpsRecord } from './consolidation-ops-records.js';
|
|
20
|
+
import { checkConsolidationCandidates, shouldUseMetaIndex } from './consolidation-trigger.js';
|
|
21
|
+
/**
|
|
22
|
+
* 清理当前 agent 的旧整理批次。
|
|
23
|
+
* 启动新整理时调用,保留最新的一个会话历史用于查看和调试。
|
|
24
|
+
*/
|
|
25
|
+
function cleanupPreviousConsolidationBatches(selfAid) {
|
|
26
|
+
try {
|
|
27
|
+
const batchRoot = path.join(resolveRoot(), 'memory', 'consolidation-batches');
|
|
28
|
+
if (!fs.existsSync(batchRoot))
|
|
29
|
+
return;
|
|
30
|
+
// 匹配格式:YYYYMMDDTHHMMSS-<selfAid>-*
|
|
31
|
+
const escapedAid = selfAid.replace(/\./g, '\\.');
|
|
32
|
+
const pattern = new RegExp(`^\\d{8}T\\d{6}-${escapedAid}-`);
|
|
33
|
+
const entries = fs.readdirSync(batchRoot, { withFileTypes: true })
|
|
34
|
+
.filter(e => e.isDirectory() && pattern.test(e.name))
|
|
35
|
+
.map(e => ({ name: e.name, path: path.join(batchRoot, e.name) }))
|
|
36
|
+
.sort((a, b) => b.name.localeCompare(a.name)); // 最新的在前
|
|
37
|
+
// 保留最新的一个,删除其他
|
|
38
|
+
for (let i = 1; i < entries.length; i++) {
|
|
39
|
+
const dir = entries[i];
|
|
40
|
+
try {
|
|
41
|
+
fs.rmSync(dir.path, { recursive: true, force: true });
|
|
42
|
+
logger.debug(`[Memory] 清理前一次整理批次: ${dir.name}`);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
logger.warn(`[Memory] 清理前一次整理批次失败 ${dir.name}: ${e?.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
logger.warn(`[Memory] 清理整理批次时出错: ${e?.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function buildDebugTaskContext(input) {
|
|
54
|
+
const { session } = input;
|
|
55
|
+
const meta = (session.metadata ?? {});
|
|
56
|
+
const sessionType = session.sessionType ?? meta.sessionType ?? 'main';
|
|
57
|
+
let channel = session.channelType || session.channel;
|
|
58
|
+
let chatType = session.chatType || 'private';
|
|
59
|
+
let peerId = meta.peerId;
|
|
60
|
+
let peerKeyId = chatType === 'group'
|
|
61
|
+
? (meta.groupId || session.channelId)
|
|
62
|
+
: (meta.peerId || session.channelId);
|
|
63
|
+
let threadId = session.threadId || undefined;
|
|
64
|
+
if (session.channelId?.startsWith('trigger:') && session.selfAID && input.getTrigger) {
|
|
65
|
+
const triggerId = session.channelId.slice('trigger:'.length);
|
|
66
|
+
const origin = input.getTrigger(session.selfAID, triggerId)?.origin;
|
|
67
|
+
if (origin?.channelType) {
|
|
68
|
+
channel = origin.channelType;
|
|
69
|
+
chatType = origin.delivery?.chatType ?? 'private';
|
|
70
|
+
peerId = origin.peerId;
|
|
71
|
+
peerKeyId = chatType === 'group' ? origin.channelId : (origin.peerId ?? origin.channelId);
|
|
72
|
+
threadId = origin.threadId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
sessionId: session.id,
|
|
77
|
+
baseagent: session.baseagent,
|
|
78
|
+
channel,
|
|
79
|
+
channelId: session.channelId,
|
|
80
|
+
chatType,
|
|
81
|
+
selfAid: session.selfAID,
|
|
82
|
+
peerId,
|
|
83
|
+
peerKey: channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined,
|
|
84
|
+
peerName: meta.peerName,
|
|
85
|
+
peerType: meta.peerType,
|
|
86
|
+
threadId,
|
|
87
|
+
sessionType,
|
|
88
|
+
memoryMode: meta.memoryMode,
|
|
89
|
+
memoryScope: meta.memoryScope,
|
|
90
|
+
consolidationBatchDir: meta.consolidationBatchDir ?? meta.consolidationLogBase,
|
|
91
|
+
sessionRuntimeDir: input.sessionRuntimeDir,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 序列化上下文并直接创建整理会话。
|
|
96
|
+
* 失败抛错,调用方负责决定是否记录诊断或回传错误。
|
|
97
|
+
*/
|
|
98
|
+
export async function launchConsolidation(channel, params) {
|
|
99
|
+
const { taskCtx, mode, triggeredBy, ownerAid } = params;
|
|
100
|
+
const effectiveCtx = params.peerKeyOverride
|
|
101
|
+
? { ...taskCtx, peerKey: params.peerKeyOverride }
|
|
102
|
+
: taskCtx;
|
|
103
|
+
const selfAid = effectiveCtx.selfAid;
|
|
104
|
+
if (!selfAid)
|
|
105
|
+
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
106
|
+
// 清理旧的整理批次(保留最新的一个历史)
|
|
107
|
+
cleanupPreviousConsolidationBatches(selfAid);
|
|
108
|
+
// notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
|
|
109
|
+
let memoryDir;
|
|
110
|
+
if (mode === 'notes') {
|
|
111
|
+
memoryDir = effectiveCtx.peerKey
|
|
112
|
+
? path.join(resolveRoot(), 'agents', selfAid, 'relations', effectiveCtx.peerKey, 'memory')
|
|
113
|
+
: path.join(resolveRoot(), 'agents', selfAid, 'personal', 'memory');
|
|
114
|
+
const pendingDir = path.join(memoryDir, 'pending');
|
|
115
|
+
const processingDir = path.join(memoryDir, 'processing');
|
|
116
|
+
if (fs.existsSync(processingDir)) {
|
|
117
|
+
// 上一批还没收尾(会话仍在跑,或 daemon 中途重启丢了收尾),不覆盖、不合并
|
|
118
|
+
logger.warn(`[Memory] processing/ 已存在,跳过本次迁移(上一批未收尾): ${processingDir}`);
|
|
119
|
+
}
|
|
120
|
+
else if (fs.existsSync(pendingDir)) {
|
|
121
|
+
try {
|
|
122
|
+
const count = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md')).length;
|
|
123
|
+
fs.renameSync(pendingDir, processingDir);
|
|
124
|
+
// 立即重建空 pending 目录,供主会话后续写 note
|
|
125
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
126
|
+
logger.info(`[Memory] pending → processing 快照完成(${count} 条 note): ${processingDir}`);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
// 迁移失败不阻断整理会话启动,整理会话会发现 processing 目录空并输出 NOOP
|
|
130
|
+
logger.warn(`[Memory] pending → processing 迁移失败: ${e?.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const ctxFile = serializeConsolidationContext({
|
|
135
|
+
mode,
|
|
136
|
+
triggeredBy,
|
|
137
|
+
taskCtx: effectiveCtx,
|
|
138
|
+
ownerAid,
|
|
139
|
+
sessionRuntimeDir: effectiveCtx.sessionRuntimeDir,
|
|
140
|
+
ownerInvolved: params.ownerInvolved,
|
|
141
|
+
forgetAfterDays: params.forgetAfterDays,
|
|
142
|
+
scopeOverride: params.scopeOverride,
|
|
143
|
+
});
|
|
144
|
+
const baseagent = effectiveCtx.baseagent ?? 'claude';
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const scopeTag = params.scopeOverride ? `-${params.scopeOverride}` : '';
|
|
147
|
+
const triggerId = `memory-consolidate-${mode}${scopeTag}-${now.toString(36)}`;
|
|
148
|
+
// 整理会话的渠道信息(origin):发起时抓取,供反馈时直接投递到原会话渠道。
|
|
149
|
+
// 用 channelId/peerId/threadId 三要素——不依赖 evolcore sessionId,renew/`/new` 换会话也不影响送达。
|
|
150
|
+
const feedbackOrigin = effectiveCtx.channel && effectiveCtx.peerId ? {
|
|
151
|
+
channelKey: formatChannelKey({ type: effectiveCtx.channel, selfAID: selfAid, name: 'main' }),
|
|
152
|
+
channelId: effectiveCtx.peerId,
|
|
153
|
+
channelType: effectiveCtx.channel,
|
|
154
|
+
peerId: effectiveCtx.peerId,
|
|
155
|
+
session: effectiveCtx.threadId ? 'thread' : 'main',
|
|
156
|
+
...(effectiveCtx.threadId ? { threadId: effectiveCtx.threadId } : {}),
|
|
157
|
+
delivery: { chatType: effectiveCtx.chatType ?? 'private' },
|
|
158
|
+
} : undefined;
|
|
159
|
+
// 构造 TriggerDefinition 形状的数据载体。
|
|
160
|
+
// daemonChannel.converse() 从 ctx.trigger 读 execution.memoryMode / id / name / agentAid。
|
|
161
|
+
// 不再调 manager.create()——本函数直接创建会话,不进 trigger 系统的调度/持久化。
|
|
162
|
+
const triggerDef = normalizeTriggerDefinition({
|
|
163
|
+
$schema_version: 3.1,
|
|
164
|
+
id: triggerId,
|
|
165
|
+
agentAid: selfAid,
|
|
166
|
+
enabled: true,
|
|
167
|
+
name: triggerId,
|
|
168
|
+
description: `记忆整理(mode=${mode}${effectiveCtx.peerKey ? `,peerKey=${effectiveCtx.peerKey}` : ''})`,
|
|
169
|
+
createdAt: now,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
source: { type: 'once' },
|
|
172
|
+
execution: {
|
|
173
|
+
type: 'trigger_session',
|
|
174
|
+
prompt: [
|
|
175
|
+
'你正在执行记忆整理任务。',
|
|
176
|
+
`本次整理模式:${mode}`,
|
|
177
|
+
`整理上下文文件路径:${ctxFile.filePath}`,
|
|
178
|
+
'',
|
|
179
|
+
'请先读取上述上下文文件,再按照注入的整理规范完成本次整理。',
|
|
180
|
+
].join('\n'),
|
|
181
|
+
baseagent,
|
|
182
|
+
thread: 'by_run',
|
|
183
|
+
onError: 'retry',
|
|
184
|
+
noopSentinel: '[[NOOP]]',
|
|
185
|
+
memoryMode: mode,
|
|
186
|
+
...(params.scopeOverride ? { memoryScope: params.scopeOverride } : {}),
|
|
187
|
+
// 透传到 session.metadata,供 ipc.ts 写 ops 流水、收尾流程读取
|
|
188
|
+
consolidationBatchDir: ctxFile.batchDir,
|
|
189
|
+
...(ctxFile.chatDir ? { consolidationChatDir: ctxFile.chatDir } : {}),
|
|
190
|
+
...(memoryDir ? { consolidationMemoryDir: memoryDir } : {}),
|
|
191
|
+
},
|
|
192
|
+
// 不使用 trigger 的 feedback 机制;反馈由收尾流程直接调 adapter.send() 投递
|
|
193
|
+
feedback: { strategy: 'silent' },
|
|
194
|
+
...(feedbackOrigin ? { origin: feedbackOrigin } : {}),
|
|
195
|
+
reliability: {
|
|
196
|
+
concurrency: 'forbid',
|
|
197
|
+
missedPolicy: 'run_once',
|
|
198
|
+
retry: { maxAttempts: 0, backoffMs: 30_000 },
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
// normalizeTriggerDefinition 只保留白名单字段,自定义字段在其后补挂。
|
|
202
|
+
// 反馈目标(发起时渠道三元组)与触发方式,收尾流程据此投递 / 判断是否反馈。
|
|
203
|
+
if (feedbackOrigin) {
|
|
204
|
+
triggerDef.execution.feedbackOrigin = feedbackOrigin;
|
|
205
|
+
}
|
|
206
|
+
triggerDef.execution.consolidationTriggeredBy = triggeredBy;
|
|
207
|
+
// 直接创建整理会话并发送第一条消息——不再经 TriggerRuntimeScheduler。
|
|
208
|
+
// 用 getOrCreateConversationSession + converse,与旧 trigger_session 执行路径同源(见 daemon.ts)。
|
|
209
|
+
const projectPath = params.projectPath ?? path.join(resolveRoot(), 'agents', selfAid);
|
|
210
|
+
const runId = `${triggerId}-run`;
|
|
211
|
+
const session = await channel.getOrCreateConversationSession(triggerDef, projectPath, baseagent, runId);
|
|
212
|
+
const prompt = triggerDef.execution.prompt;
|
|
213
|
+
// 不 await:整理会话在后台跑,收尾由 task:completed/error/interrupted 事件驱动
|
|
214
|
+
void channel.converse(prompt, {
|
|
215
|
+
trigger: triggerDef,
|
|
216
|
+
runId,
|
|
217
|
+
firedAt: now,
|
|
218
|
+
projectPath,
|
|
219
|
+
baseagent,
|
|
220
|
+
session,
|
|
221
|
+
causation: undefined,
|
|
222
|
+
}).catch((e) => {
|
|
223
|
+
logger.warn(`[Memory] 整理会话执行失败(mode=${mode},session=${session.id}): ${e?.message}`);
|
|
224
|
+
});
|
|
225
|
+
logger.info(`[Memory] 已拉起整理会话(mode=${mode},triggeredBy=${triggeredBy},` +
|
|
226
|
+
`selfAid=${selfAid}${effectiveCtx.peerKey ? `,peerKey=${effectiveCtx.peerKey}` : ''},` +
|
|
227
|
+
`session=${session.id})`);
|
|
228
|
+
return {
|
|
229
|
+
triggerId,
|
|
230
|
+
contextFile: ctxFile.filePath,
|
|
231
|
+
contextFilePath: ctxFile.contextFilePath,
|
|
232
|
+
logFile: ctxFile.contextFilePath,
|
|
233
|
+
batchDir: ctxFile.batchDir,
|
|
234
|
+
executionSessionId: session.id,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 手动 consolidate 用:在超时内轮询等 runner 分配 agentSessionId。
|
|
239
|
+
* 拿到立即返回;超时返回已拿到的部分,不抛错。
|
|
240
|
+
*
|
|
241
|
+
* 会话创建后 executionSessionId 立刻可用,但 agentSessionId 要等 runner 初始化
|
|
242
|
+
* 并开始第一轮才写入 session 记录,所以这里必须轮询等待。
|
|
243
|
+
*/
|
|
244
|
+
export async function waitForConsolidationSession(opts) {
|
|
245
|
+
const executionSessionId = opts.executionSessionId;
|
|
246
|
+
if (!executionSessionId)
|
|
247
|
+
return {};
|
|
248
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10_000);
|
|
249
|
+
const interval = opts.intervalMs ?? 250;
|
|
250
|
+
while (true) {
|
|
251
|
+
try {
|
|
252
|
+
const agentSessionId = await opts.readAgentSessionId(executionSessionId);
|
|
253
|
+
if (agentSessionId)
|
|
254
|
+
return { executionSessionId, agentSessionId };
|
|
255
|
+
}
|
|
256
|
+
catch { /* 会话记录写到一半,下一轮再读 */ }
|
|
257
|
+
if (Date.now() >= deadline)
|
|
258
|
+
return { executionSessionId };
|
|
259
|
+
await new Promise(r => setTimeout(r, interval));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** 把 entry 移进 destDir;重名时加时间戳后缀,不覆盖已有文件。 */
|
|
263
|
+
function moveIntoDir(srcPath, destDir, stamp) {
|
|
264
|
+
const name = path.basename(srcPath);
|
|
265
|
+
let dest = path.join(destDir, name);
|
|
266
|
+
if (fs.existsSync(dest)) {
|
|
267
|
+
if (fs.statSync(srcPath).isDirectory() && fs.statSync(dest).isDirectory()) {
|
|
268
|
+
// 目录(如 pending/archive/)逐个合并子项
|
|
269
|
+
for (const child of fs.readdirSync(srcPath))
|
|
270
|
+
moveIntoDir(path.join(srcPath, child), dest, stamp);
|
|
271
|
+
fs.rmdirSync(srcPath);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const ext = path.extname(name);
|
|
275
|
+
dest = path.join(destDir, `${path.basename(name, ext)}-${stamp}${ext}`);
|
|
276
|
+
}
|
|
277
|
+
fs.renameSync(srcPath, dest);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 整理会话结束后的收尾,task:completed / task:error / task:interrupted 都走这里。
|
|
281
|
+
* - processing/ 里的 note:成功 → archive/notes/,失败 → 归还 pending/(下次重试)
|
|
282
|
+
* - processing/ 里的其他条目(如 pending/archive/)原样还回 pending/
|
|
283
|
+
* - 成功才写 checkpoint,失败时下次整理仍取同一段增量消息
|
|
284
|
+
* - ops 汇总无论成败都写
|
|
285
|
+
* 幂等:processing/ 不存在时只写汇总。
|
|
286
|
+
*/
|
|
287
|
+
export async function onConsolidationComplete(params) {
|
|
288
|
+
const now = params.now ?? Date.now();
|
|
289
|
+
const stamp = new Date(now).toISOString().replace(/[-:]/g, '').slice(0, 15);
|
|
290
|
+
const result = { moved: 0, checkpointWritten: false };
|
|
291
|
+
if (params.memoryDir) {
|
|
292
|
+
const processingDir = path.join(params.memoryDir, 'processing');
|
|
293
|
+
if (fs.existsSync(processingDir)) {
|
|
294
|
+
const pendingDir = path.join(params.memoryDir, 'pending');
|
|
295
|
+
const noteDest = params.success ? path.join(params.memoryDir, 'archive', 'notes') : pendingDir;
|
|
296
|
+
fs.mkdirSync(noteDest, { recursive: true });
|
|
297
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
298
|
+
for (const entry of fs.readdirSync(processingDir)) {
|
|
299
|
+
const src = path.join(processingDir, entry);
|
|
300
|
+
const isNote = entry.endsWith('.md') && fs.statSync(src).isFile();
|
|
301
|
+
moveIntoDir(src, isNote ? noteDest : pendingDir, stamp);
|
|
302
|
+
if (isNote)
|
|
303
|
+
result.moved++;
|
|
304
|
+
}
|
|
305
|
+
fs.rmdirSync(processingDir);
|
|
306
|
+
result.movedTo = noteDest;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (params.success && params.chatDir) {
|
|
310
|
+
const { writeCheckpoint } = await import('./consolidation-context.js');
|
|
311
|
+
writeCheckpoint(params.chatDir, now);
|
|
312
|
+
result.checkpointWritten = true;
|
|
313
|
+
}
|
|
314
|
+
const { summarizeOpsRecords, appendOpsRecord } = await import('./consolidation-ops-records.js');
|
|
315
|
+
appendOpsRecord(params.batchDir, summarizeOpsRecords(params.batchDir));
|
|
316
|
+
logger.info(`[Memory] 整理收尾(${params.success ? '成功' : '失败'}):归位 ${result.moved} 条 note` +
|
|
317
|
+
`${result.movedTo ? ` → ${result.movedTo}` : ''},checkpoint=${result.checkpointWritten}`);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
/** 会话结束后把产出回填到批次任务书。 */
|
|
321
|
+
export function recordConsolidationOutcome(contextFilePath, outcome) {
|
|
322
|
+
try {
|
|
323
|
+
fs.appendFileSync(contextFilePath, `\n---\n\n## 本次产出\n\n${outcome}\n`, 'utf-8');
|
|
324
|
+
}
|
|
325
|
+
catch { /* ignore */ }
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 整理会话失败/中断时,依据 ops.jsonl 逐条回撤成功的写操作。
|
|
329
|
+
* 回撤顺序:倒序(最后写的先撤)。
|
|
330
|
+
*
|
|
331
|
+
* create → 删除写入的文件,若有 archivedFiles 将其还原回 consolidated/
|
|
332
|
+
* update → 用 prevTags 重写文件(恢复修改前的标签,不改 updatedAt 以外的内容)
|
|
333
|
+
* archive → 将文件从 archivedTo 移回 sourceFile
|
|
334
|
+
*/
|
|
335
|
+
export async function rollbackConsolidation(batchDir, selfAid) {
|
|
336
|
+
const { readRollbackRecords, appendOpsRecord } = await import('./consolidation-ops-records.js');
|
|
337
|
+
const entries = readRollbackRecords(batchDir);
|
|
338
|
+
let rolledBack = 0;
|
|
339
|
+
const errors = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
try {
|
|
342
|
+
if (entry.op === 'create') {
|
|
343
|
+
// 删除写入的文件
|
|
344
|
+
if (entry.filePath && fs.existsSync(entry.filePath)) {
|
|
345
|
+
fs.unlinkSync(entry.filePath);
|
|
346
|
+
}
|
|
347
|
+
// 还原被归档的旧文件
|
|
348
|
+
for (const af of (entry.archivedFiles ?? [])) {
|
|
349
|
+
if (af.archivedTo && fs.existsSync(af.archivedTo)) {
|
|
350
|
+
const dest = af.source.includes('/') || af.source.includes('\\')
|
|
351
|
+
? af.source
|
|
352
|
+
: path.join(path.dirname(af.archivedTo), path.basename(af.archivedTo));
|
|
353
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
354
|
+
fs.renameSync(af.archivedTo, dest);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
rolledBack++;
|
|
358
|
+
}
|
|
359
|
+
else if (entry.op === 'update') {
|
|
360
|
+
// 恢复修改前的 tags:读文件、替换 tags 行、重写(不重签名,仅文本恢复)
|
|
361
|
+
const target = entry.target;
|
|
362
|
+
if (target && fs.existsSync(target)) {
|
|
363
|
+
const raw = fs.readFileSync(target, 'utf-8');
|
|
364
|
+
const prevTags = entry.prevTags ?? [];
|
|
365
|
+
const tagsLine = prevTags.length > 0 ? `tags: [${prevTags.join(', ')}]` : '';
|
|
366
|
+
// 替换 tags: [...] 行;若 prevTags 为空则移除该行
|
|
367
|
+
const updated = raw.replace(/^tags: \[.*\]$/m, tagsLine).replace(/^\n$/, '');
|
|
368
|
+
fs.writeFileSync(target, updated, 'utf-8');
|
|
369
|
+
rolledBack++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (entry.op === 'archive') {
|
|
373
|
+
// 将文件从 archivedTo 移回 sourceFile
|
|
374
|
+
if (entry.archivedTo && entry.sourceFile && fs.existsSync(entry.archivedTo)) {
|
|
375
|
+
fs.mkdirSync(path.dirname(entry.sourceFile), { recursive: true });
|
|
376
|
+
fs.renameSync(entry.archivedTo, entry.sourceFile);
|
|
377
|
+
rolledBack++;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
const msg = `回撤 ${entry.op} "${entry.target}" 失败: ${e?.message}`;
|
|
383
|
+
errors.push(msg);
|
|
384
|
+
logger.warn(`[Memory] ${msg}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
appendOpsRecord(batchDir, {
|
|
388
|
+
ts: new Date().toISOString(),
|
|
389
|
+
op: 'session-end',
|
|
390
|
+
success: false,
|
|
391
|
+
movedNotes: 0,
|
|
392
|
+
checkpointWritten: false,
|
|
393
|
+
});
|
|
394
|
+
logger.info(`[Memory] 回撤完成:回撤 ${rolledBack} 条操作,${errors.length} 个失败`);
|
|
395
|
+
return { rolledBack, errors };
|
|
396
|
+
}
|
|
397
|
+
const MAX_CONCURRENT_CONSOLIDATION = 5;
|
|
398
|
+
export function createConsolidationSessionFinalizer({ sessionManager, getAgent, agentMap, releaseSlot, enqueueConsolidation, sendFeedback, }) {
|
|
399
|
+
return async (sessionId, success) => {
|
|
400
|
+
let releaseAid;
|
|
401
|
+
try {
|
|
402
|
+
if (!sessionId)
|
|
403
|
+
return;
|
|
404
|
+
const session = await sessionManager.getSessionById(sessionId);
|
|
405
|
+
if (!session)
|
|
406
|
+
return;
|
|
407
|
+
const meta = (session.metadata ?? {});
|
|
408
|
+
const batchDir = (meta.consolidationBatchDir ?? meta.consolidationLogBase);
|
|
409
|
+
if (!batchDir)
|
|
410
|
+
return;
|
|
411
|
+
releaseAid = session.selfAID;
|
|
412
|
+
const { summarizeOpsRecords, collectAffectedScopes } = await import('./consolidation-ops-records.js');
|
|
413
|
+
if (!success) {
|
|
414
|
+
await rollbackConsolidation(batchDir, session.selfAID);
|
|
415
|
+
}
|
|
416
|
+
const completeResult = await onConsolidationComplete({
|
|
417
|
+
success,
|
|
418
|
+
batchDir,
|
|
419
|
+
chatDir: meta.consolidationChatDir,
|
|
420
|
+
memoryDir: meta.consolidationMemoryDir,
|
|
421
|
+
});
|
|
422
|
+
try {
|
|
423
|
+
const affectedScopes = collectAffectedScopes(batchDir);
|
|
424
|
+
if (affectedScopes.size > 0) {
|
|
425
|
+
const { rebuildIndex } = await import('./index-manager.js');
|
|
426
|
+
const selfAid = session.selfAID;
|
|
427
|
+
const memoryConfig = selfAid ? getAgent(selfAid)?.config.memory : undefined;
|
|
428
|
+
const maxBytes = memoryConfig?.indexMaxBytes ?? 5 * 1024;
|
|
429
|
+
for (const [scope, memoryDir] of affectedScopes) {
|
|
430
|
+
const consolidatedDir = path.join(memoryDir, 'consolidated');
|
|
431
|
+
const indexPath = path.join(memoryDir, 'MEMORY.md');
|
|
432
|
+
const invalidDir = path.join(memoryDir, 'archive', 'invalid');
|
|
433
|
+
try {
|
|
434
|
+
await rebuildIndex(consolidatedDir, indexPath, scope, invalidDir, undefined, maxBytes);
|
|
435
|
+
}
|
|
436
|
+
catch { }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch { }
|
|
441
|
+
appendOpsRecord(batchDir, {
|
|
442
|
+
ts: new Date().toISOString(),
|
|
443
|
+
op: 'session-end',
|
|
444
|
+
success,
|
|
445
|
+
movedNotes: completeResult.moved,
|
|
446
|
+
checkpointWritten: completeResult.checkpointWritten,
|
|
447
|
+
});
|
|
448
|
+
if (success) {
|
|
449
|
+
const memoryMode = meta.memoryMode;
|
|
450
|
+
if (memoryMode && memoryMode !== 'forget') {
|
|
451
|
+
const opsSummary = summarizeOpsRecords(batchDir);
|
|
452
|
+
const hasOutput = completeResult.moved > 0 || opsSummary.created > 0 || opsSummary.updated > 0;
|
|
453
|
+
if (hasOutput) {
|
|
454
|
+
try {
|
|
455
|
+
const { collectCandidates, isDueForReview, deferCandidate } = await import('./staleness.js');
|
|
456
|
+
const selfAid = session.selfAID;
|
|
457
|
+
if (selfAid) {
|
|
458
|
+
const memoryConfig = getAgent(selfAid)?.config.memory;
|
|
459
|
+
const thresholds = { forgetAfterDays: memoryConfig?.forgetAfterDays ?? 3 };
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
const affectedScopes = collectAffectedScopes(batchDir);
|
|
462
|
+
for (const [scope, memoryDir] of affectedScopes) {
|
|
463
|
+
const consolidatedDir = path.join(memoryDir, 'consolidated');
|
|
464
|
+
let due = 0;
|
|
465
|
+
try {
|
|
466
|
+
for (const candidate of collectCandidates(consolidatedDir, thresholds, now)) {
|
|
467
|
+
if (isDueForReview(candidate, thresholds, now))
|
|
468
|
+
due++;
|
|
469
|
+
else {
|
|
470
|
+
try {
|
|
471
|
+
deferCandidate(candidate.filePath, now);
|
|
472
|
+
}
|
|
473
|
+
catch { }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (due > 0) {
|
|
481
|
+
const ownerAid = memoryConfig ? getAgent(selfAid)?.config.owners?.[0] : undefined;
|
|
482
|
+
const taskCtx = { selfAid, peerKey: meta.peerKey };
|
|
483
|
+
void enqueueConsolidation({
|
|
484
|
+
taskCtx,
|
|
485
|
+
mode: 'forget',
|
|
486
|
+
triggeredBy: 'periodic-trigger',
|
|
487
|
+
ownerAid,
|
|
488
|
+
forgetAfterDays: memoryConfig?.forgetAfterDays,
|
|
489
|
+
scopeOverride: scope,
|
|
490
|
+
}).catch(() => { });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch { }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const agentSessionId = session.agentSessionId;
|
|
500
|
+
const projectPath = session.projectPath;
|
|
501
|
+
const baseagent = session.baseagent ?? 'claude';
|
|
502
|
+
if (agentSessionId && projectPath) {
|
|
503
|
+
const historyPath = path.join(batchDir, 'history.jsonl');
|
|
504
|
+
const runner = agentMap.get(`${session.selfAID}::${baseagent}`);
|
|
505
|
+
if (runner?.getSessionMessages) {
|
|
506
|
+
try {
|
|
507
|
+
const messages = await runner.getSessionMessages(agentSessionId, projectPath);
|
|
508
|
+
fs.writeFileSync(historyPath, messages.map((message) => JSON.stringify(message)).join('\n') + '\n', 'utf-8');
|
|
509
|
+
}
|
|
510
|
+
catch { }
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
fs.writeFileSync(historyPath, JSON.stringify({ note: `runner ${baseagent} does not support getSessionMessages` }) + '\n', 'utf-8');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const feedbackOrigin = meta.feedbackOrigin;
|
|
517
|
+
const triggeredBy = meta.consolidationTriggeredBy;
|
|
518
|
+
if (feedbackOrigin && triggeredBy && triggeredBy !== 'periodic-trigger' && session.selfAID) {
|
|
519
|
+
try {
|
|
520
|
+
const { readFeedbackText } = await import('./consolidation-ops-records.js');
|
|
521
|
+
const customText = readFeedbackText(batchDir);
|
|
522
|
+
const text = customText ?? renderDefaultConsolidationFeedback({
|
|
523
|
+
mode: meta.memoryMode,
|
|
524
|
+
triggeredBy,
|
|
525
|
+
success,
|
|
526
|
+
summary: summarizeOpsRecords(batchDir),
|
|
527
|
+
batchDir,
|
|
528
|
+
});
|
|
529
|
+
await sendFeedback(feedbackOrigin, text, session.selfAID);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
logger.warn(`[Memory] 整理反馈投递失败(session=${sessionId}): ${error?.message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
logger.warn(`[Memory] 整理收尾失败(session=${sessionId}): ${error?.message}`);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
if (releaseAid)
|
|
541
|
+
releaseSlot(releaseAid);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
export function createConsolidationFeedbackSender(getChannel) {
|
|
546
|
+
return async (origin, text, selfAid) => {
|
|
547
|
+
const { buildEnvelope } = await import('../message/message-utils.js');
|
|
548
|
+
const channelKey = origin.channelKey;
|
|
549
|
+
const channelId = origin.channelId;
|
|
550
|
+
if (!channelKey || !channelId) {
|
|
551
|
+
logger.warn('[Memory] 反馈目标缺少 channelKey/channelId,跳过投递');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const binding = getChannel(selfAid, channelKey);
|
|
555
|
+
if (!binding) {
|
|
556
|
+
logger.warn(`[Memory] 找不到渠道绑定(aid=${selfAid},channelKey=${channelKey}),跳过反馈`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const threadId = origin.threadId;
|
|
560
|
+
const chatType = origin.delivery?.chatType;
|
|
561
|
+
const replyContext = {};
|
|
562
|
+
if (threadId)
|
|
563
|
+
replyContext.threadId = threadId;
|
|
564
|
+
if (chatType)
|
|
565
|
+
replyContext.delivery = { chatType };
|
|
566
|
+
await binding.adapter.send(buildEnvelope({
|
|
567
|
+
taskId: `memory-feedback-${Date.now()}`,
|
|
568
|
+
channel: binding.adapter.channelKey,
|
|
569
|
+
channelId,
|
|
570
|
+
agentName: binding.agentName,
|
|
571
|
+
replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
|
|
572
|
+
}), {
|
|
573
|
+
kind: 'result.text',
|
|
574
|
+
text,
|
|
575
|
+
isFinal: true,
|
|
576
|
+
format: 'markdown',
|
|
577
|
+
});
|
|
578
|
+
logger.info(`[Memory] 整理反馈已投递(aid=${selfAid},channelId=${channelId})`);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
export function registerConsolidationSessionEvents({ eventBus, permissionGateway, finalize, }) {
|
|
582
|
+
eventBus.subscribe('task:interrupted', event => {
|
|
583
|
+
if ('sessionId' in event && event.sessionId) {
|
|
584
|
+
permissionGateway.cancelAll(event.sessionId, event.reason || 'interrupted');
|
|
585
|
+
}
|
|
586
|
+
void finalize(event.sessionId, false);
|
|
587
|
+
});
|
|
588
|
+
eventBus.subscribe('task:error', event => {
|
|
589
|
+
if ('sessionId' in event && event.sessionId) {
|
|
590
|
+
permissionGateway.cancelAll(event.sessionId, 'task_error', false);
|
|
591
|
+
}
|
|
592
|
+
void finalize(event.sessionId, false);
|
|
593
|
+
});
|
|
594
|
+
eventBus.subscribe('task:completed', event => {
|
|
595
|
+
if ('sessionId' in event && event.sessionId) {
|
|
596
|
+
permissionGateway.cancelAll(event.sessionId, 'task_completed', false);
|
|
597
|
+
}
|
|
598
|
+
void finalize(event.sessionId, true);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
export class ConsolidationQueue {
|
|
602
|
+
daemonChannel;
|
|
603
|
+
activeAgents = new Set();
|
|
604
|
+
queues = new Map();
|
|
605
|
+
constructor(daemonChannel) {
|
|
606
|
+
this.daemonChannel = daemonChannel;
|
|
607
|
+
}
|
|
608
|
+
async enqueue(params) {
|
|
609
|
+
const aid = params.taskCtx.selfAid;
|
|
610
|
+
if (!aid)
|
|
611
|
+
return null;
|
|
612
|
+
const agentBusy = this.activeAgents.has(aid);
|
|
613
|
+
const queue = this.queues.get(aid) ?? [];
|
|
614
|
+
const globalFull = this.activeAgents.size >= MAX_CONCURRENT_CONSOLIDATION;
|
|
615
|
+
if (!agentBusy && !globalFull && queue.length === 0) {
|
|
616
|
+
return await this.start(params);
|
|
617
|
+
}
|
|
618
|
+
queue.push(params);
|
|
619
|
+
this.queues.set(aid, queue);
|
|
620
|
+
logger.info(`[Memory] 整理请求入队(aid=${aid},mode=${params.mode},` +
|
|
621
|
+
`队列长度=${queue.length},全局活跃=${this.activeAgents.size})`);
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
release(aid) {
|
|
625
|
+
this.activeAgents.delete(aid);
|
|
626
|
+
void this.drain();
|
|
627
|
+
}
|
|
628
|
+
async start(params) {
|
|
629
|
+
const aid = params.taskCtx.selfAid;
|
|
630
|
+
if (!aid)
|
|
631
|
+
return null;
|
|
632
|
+
this.activeAgents.add(aid);
|
|
633
|
+
try {
|
|
634
|
+
return await launchConsolidation(this.daemonChannel, params);
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
logger.warn(`[Memory] 拉起整理会话失败(aid=${aid},mode=${params.mode}): ${error?.message}`);
|
|
638
|
+
this.activeAgents.delete(aid);
|
|
639
|
+
void this.drain();
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async drain() {
|
|
644
|
+
while (this.activeAgents.size < MAX_CONCURRENT_CONSOLIDATION) {
|
|
645
|
+
let picked;
|
|
646
|
+
for (const [aid, queue] of this.queues) {
|
|
647
|
+
if (queue.length === 0)
|
|
648
|
+
continue;
|
|
649
|
+
if (this.activeAgents.has(aid))
|
|
650
|
+
continue;
|
|
651
|
+
picked = { aid, params: queue.shift() };
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
if (!picked)
|
|
655
|
+
break;
|
|
656
|
+
if (this.queues.get(picked.aid)?.length === 0)
|
|
657
|
+
this.queues.delete(picked.aid);
|
|
658
|
+
await this.start(picked.params);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
export function registerConsolidationLaunchExecutor({ ipcServer, sessionManager, enqueueConsolidation, }) {
|
|
663
|
+
ipcServer.setConsolidationLaunchExecutor(async (params) => {
|
|
664
|
+
if (!params.taskCtx.selfAid)
|
|
665
|
+
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
666
|
+
const launched = await enqueueConsolidation(params);
|
|
667
|
+
if (!launched) {
|
|
668
|
+
return {
|
|
669
|
+
triggerId: '',
|
|
670
|
+
contextFile: '',
|
|
671
|
+
contextFilePath: '',
|
|
672
|
+
logFile: '',
|
|
673
|
+
batchDir: '',
|
|
674
|
+
queued: true,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
appendOpsRecord(launched.batchDir, {
|
|
678
|
+
ts: new Date().toISOString(),
|
|
679
|
+
op: 'session-start',
|
|
680
|
+
sessionId: launched.triggerId,
|
|
681
|
+
mode: params.mode,
|
|
682
|
+
triggeredBy: params.triggeredBy,
|
|
683
|
+
});
|
|
684
|
+
const ids = await waitForConsolidationSession({
|
|
685
|
+
executionSessionId: launched.executionSessionId,
|
|
686
|
+
readAgentSessionId: async (id) => (await sessionManager.getSessionById(id))?.agentSessionId,
|
|
687
|
+
});
|
|
688
|
+
return { ...launched, ...ids };
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, enqueueConsolidation, }) {
|
|
692
|
+
ipcServer.setConsolidationCheckExecutor(async ({ selfAid }) => {
|
|
693
|
+
const agentConfig = agentRegistry.get(selfAid)?.config;
|
|
694
|
+
if (agentConfig?.memory?.enableRelation === false)
|
|
695
|
+
return { launched: [] };
|
|
696
|
+
const ownerAid = agentConfig?.owners?.[0];
|
|
697
|
+
const agentCount = agentRegistry.runnableAgents().length;
|
|
698
|
+
const relationsDir = path.join(agentDir(selfAid), 'relations');
|
|
699
|
+
let relationCount = 0;
|
|
700
|
+
try {
|
|
701
|
+
relationCount = fs.existsSync(relationsDir)
|
|
702
|
+
? fs.readdirSync(relationsDir, { withFileTypes: true }).filter(entry => entry.isDirectory()).length
|
|
703
|
+
: 0;
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// The relation directory is optional during initial bootstrap.
|
|
707
|
+
}
|
|
708
|
+
const useMetaIndex = shouldUseMetaIndex(agentCount, relationCount);
|
|
709
|
+
const candidates = checkConsolidationCandidates({
|
|
710
|
+
selfAid,
|
|
711
|
+
ownerAid,
|
|
712
|
+
config: agentConfig?.memory,
|
|
713
|
+
useMetaIndex,
|
|
714
|
+
});
|
|
715
|
+
const launched = [];
|
|
716
|
+
for (const candidate of candidates) {
|
|
717
|
+
const taskCtx = {
|
|
718
|
+
selfAid,
|
|
719
|
+
peerKey: candidate.peerKey,
|
|
720
|
+
baseagent: undefined,
|
|
721
|
+
sessionId: undefined,
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
await enqueueConsolidation({
|
|
725
|
+
taskCtx,
|
|
726
|
+
mode: candidate.mode,
|
|
727
|
+
triggeredBy: 'periodic-trigger',
|
|
728
|
+
ownerAid,
|
|
729
|
+
forgetAfterDays: agentConfig?.memory?.forgetAfterDays,
|
|
730
|
+
...(candidate.scope ? { scopeOverride: candidate.scope } : {}),
|
|
731
|
+
});
|
|
732
|
+
launched.push({ mode: candidate.mode, ...(candidate.peerKey ? { peerKey: candidate.peerKey } : {}) });
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
logger.warn(`[Memory] 定时检查入队失败(aid=${selfAid},mode=${candidate.mode}): ${error?.message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (launched.length > 0) {
|
|
739
|
+
logger.info(`[Memory] 定时检查拉起 ${launched.length} 个整理会话(aid=${selfAid}): `
|
|
740
|
+
+ launched.map(item => `${item.mode}${item.peerKey ? `(${item.peerKey})` : ''}`).join(', '));
|
|
741
|
+
}
|
|
742
|
+
return { launched };
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
export function registerRelationConsolidationEvents({ eventBus, agentRegistry, responseEngine, enqueueConsolidation, }) {
|
|
746
|
+
const triggerRelationConsolidation = async (taskCtx, triggeredBy) => {
|
|
747
|
+
try {
|
|
748
|
+
if (!taskCtx?.selfAid || !taskCtx?.peerKey)
|
|
749
|
+
return;
|
|
750
|
+
if (taskCtx.sessionType === 'memory')
|
|
751
|
+
return;
|
|
752
|
+
const memoryConfig = agentRegistry.get(taskCtx.selfAid)?.config?.memory;
|
|
753
|
+
if (memoryConfig?.enableRelation === false)
|
|
754
|
+
return;
|
|
755
|
+
const ownerAid = agentRegistry.get(taskCtx.selfAid)?.config?.owners?.[0];
|
|
756
|
+
await enqueueConsolidation({
|
|
757
|
+
taskCtx,
|
|
758
|
+
mode: 'notes',
|
|
759
|
+
triggeredBy,
|
|
760
|
+
ownerAid,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
logger.warn(`[MemoryT17] relation 整理触发失败(${triggeredBy}): ${error?.message}`);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
eventBus.subscribe('runner:compact-start', event => {
|
|
768
|
+
const compactEvent = event;
|
|
769
|
+
if (!compactEvent.sessionId)
|
|
770
|
+
return;
|
|
771
|
+
void triggerRelationConsolidation(responseEngine.getTaskRuntimeContext(compactEvent.sessionId), 'compress');
|
|
772
|
+
});
|
|
773
|
+
eventBus.subscribe('session:created', event => {
|
|
774
|
+
const sessionEvent = event;
|
|
775
|
+
if (!sessionEvent.sourceSessionId)
|
|
776
|
+
return;
|
|
777
|
+
let snapshot;
|
|
778
|
+
if (sessionEvent.sourceChannel && sessionEvent.sourceChannelId && sessionEvent.sourceSelfAid) {
|
|
779
|
+
const peerKeyId = sessionEvent.sourceChatType === 'group'
|
|
780
|
+
? sessionEvent.sourceChannelId
|
|
781
|
+
: (sessionEvent.sourcePeerId ?? sessionEvent.sourceChannelId);
|
|
782
|
+
const peerKey = peerKeyId
|
|
783
|
+
? `${sessionEvent.sourceChannel}#${encodeURIComponent(peerKeyId)}`
|
|
784
|
+
: undefined;
|
|
785
|
+
snapshot = {
|
|
786
|
+
sessionId: sessionEvent.sourceSessionId,
|
|
787
|
+
selfAid: sessionEvent.sourceSelfAid,
|
|
788
|
+
peerKey,
|
|
789
|
+
channel: sessionEvent.sourceChannel,
|
|
790
|
+
channelId: sessionEvent.sourceChannelId,
|
|
791
|
+
peerId: sessionEvent.sourcePeerId ?? sessionEvent.sourceChannelId,
|
|
792
|
+
chatType: sessionEvent.sourceChatType,
|
|
793
|
+
threadId: sessionEvent.sourceThreadId,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
const taskCtx = snapshot ?? responseEngine.getTaskRuntimeContext(sessionEvent.sourceSessionId);
|
|
797
|
+
void triggerRelationConsolidation(taskCtx, 'session-new');
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/** Render the default feedback message for a completed memory consolidation. */
|
|
801
|
+
export function renderDefaultConsolidationFeedback(input) {
|
|
802
|
+
const triggerLabel = {
|
|
803
|
+
manual: '手动触发',
|
|
804
|
+
compress: '上下文压缩',
|
|
805
|
+
'session-new': '新会话',
|
|
806
|
+
'periodic-trigger': '定时检查',
|
|
807
|
+
};
|
|
808
|
+
const mode = input.mode ?? 'notes';
|
|
809
|
+
const cause = triggerLabel[input.triggeredBy] ?? input.triggeredBy;
|
|
810
|
+
if (!input.success) {
|
|
811
|
+
return [
|
|
812
|
+
'## 记忆整理失败',
|
|
813
|
+
'',
|
|
814
|
+
`**模式**:${mode}`,
|
|
815
|
+
`**触发**:${cause}`,
|
|
816
|
+
'',
|
|
817
|
+
'本次整理未完成,已回退未归档的改动。详情见整理批次:',
|
|
818
|
+
`\`${input.batchDir}\``,
|
|
819
|
+
].join('\n');
|
|
820
|
+
}
|
|
821
|
+
const { created, updated, archived, errors } = input.summary;
|
|
822
|
+
const lines = [
|
|
823
|
+
'## 记忆整理完成',
|
|
824
|
+
'',
|
|
825
|
+
`**模式**:${mode} `,
|
|
826
|
+
`**触发**:${cause}`,
|
|
827
|
+
'',
|
|
828
|
+
'### 处理结果',
|
|
829
|
+
`- 新建记忆:${created} 条`,
|
|
830
|
+
`- 更新记忆:${updated} 条`,
|
|
831
|
+
`- 归档记忆:${archived} 条`,
|
|
832
|
+
];
|
|
833
|
+
if (errors > 0)
|
|
834
|
+
lines.push(`- 错误:${errors} 条`);
|
|
835
|
+
lines.push('', `整理批次:\`${input.batchDir}\``);
|
|
836
|
+
return lines.join('\n');
|
|
837
|
+
}
|
|
838
|
+
export { resolveRoot };
|