evolcore 0.0.34 → 0.0.35
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 +22 -0
- package/bin/install-codex-managed-hooks.mjs +26 -3
- package/dist/agents/claude-runner.js +11 -1
- package/dist/agents/codex/diagnostics.js +42 -11
- package/dist/agents/codex-runner.js +27 -3
- package/dist/agents/ecagent-runner.js +17 -7
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +113 -7
- package/dist/cli/aun-commands.js +37 -13
- package/dist/cli/daemon-commands.js +10 -4
- package/dist/cli/fs-command.js +1 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +46 -15
- package/dist/cli/thread-command.js +12 -4
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/core/auth/operation-authorizer.js +21 -2
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/ipc-registration.js +11 -1
- package/dist/core/memory/batch-meta.js +1 -0
- package/dist/core/memory/consolidation-context.js +97 -14
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +55 -0
- package/dist/core/memory/memory-writer.js +20 -4
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/message-bridge.js +3 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +15 -4
- package/dist/core/permission/ec-command-parser.js +76 -40
- package/dist/core/permission/tool-policy.js +32 -2
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/session/session-manager.js +56 -1
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/system-session/memory.js +87 -20
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +246 -32
- package/dist/utils/cross-platform.js +6 -3
- package/kits/docs/evolcore/fs-architecture.md +5 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +5 -3
- package/kits/docs/evolcore/memory-consolidation.md +1 -2
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +2 -2
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +25 -9
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -0
- package/kits/eck_manifest.memory.json +20 -0
- package/kits/rules/01-overview.md +3 -3
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/skills/ec-group/SKILL.md +6 -3
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/notes.md +1 -1
- package/kits/templates/memory-consolidation/workflow.md +36 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/memory.md +2 -2
- package/package.json +1 -1
|
@@ -2542,7 +2542,11 @@ export class CommandHandler {
|
|
|
2542
2542
|
}
|
|
2543
2543
|
}
|
|
2544
2544
|
else {
|
|
2545
|
-
|
|
2545
|
+
// `--thread` 是话题作用域读取:目标对端由话题推断,日志属于本 agent 自己,
|
|
2546
|
+
// 所以既允许在群会话里发起,也允许读当前对端以外的话题历史。
|
|
2547
|
+
const threadScoped = parsed.command.kind === 'history' && !!parsed.command.threadId;
|
|
2548
|
+
if (!hasTrustedFullAccessDelegation(grant) && !threadScoped
|
|
2549
|
+
&& (grant.channelType !== 'aun' || grant.chatType !== 'private')) {
|
|
2546
2550
|
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed msg reads require the current AUN private relation' };
|
|
2547
2551
|
}
|
|
2548
2552
|
// A management role may read history for any peer, one peer per query.
|
|
@@ -2551,6 +2555,7 @@ export class CommandHandler {
|
|
|
2551
2555
|
// stay pinned here, and authorizeOperation below remains the authority
|
|
2552
2556
|
// for every role (member carries currentRelationOnly; visitor is denied).
|
|
2553
2557
|
const mayReadCrossPeer = hasTrustedFullAccessDelegation(grant)
|
|
2558
|
+
|| threadScoped
|
|
2554
2559
|
|| (parsed.command.kind === 'history' && isAdminRole(grant.issuedRole ?? 'none'));
|
|
2555
2560
|
if (!mayReadCrossPeer && parsed.command.target !== conversationId) {
|
|
2556
2561
|
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
|
|
@@ -2601,6 +2606,7 @@ export class CommandHandler {
|
|
|
2601
2606
|
...(parsed.command.before !== undefined ? { before: parsed.command.before } : {}),
|
|
2602
2607
|
...(parsed.command.after !== undefined ? { after: parsed.command.after } : {}),
|
|
2603
2608
|
...(parsed.command.direction ? { direction: parsed.command.direction } : {}),
|
|
2609
|
+
...(parsed.command.threadId ? { threadId: parsed.command.threadId } : {}),
|
|
2604
2610
|
},
|
|
2605
2611
|
},
|
|
2606
2612
|
auditMetadata: {
|
|
@@ -1769,6 +1769,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1769
1769
|
value: definition.id,
|
|
1770
1770
|
label: definition.name,
|
|
1771
1771
|
desc: `${view.scheduleType}${view.nextFireAt ? ` | 下次 ${new Date(view.nextFireAt).toLocaleString()}` : ''}`,
|
|
1772
|
+
// 只有 trigger 有说明时才带上 description 字段,前端按需渲染
|
|
1773
|
+
...(definition.description ? { description: definition.description } : {}),
|
|
1772
1774
|
status: definition.enabled ? 'active' : 'disabled',
|
|
1773
1775
|
};
|
|
1774
1776
|
});
|
|
@@ -474,18 +474,15 @@ async function summaryCommand(ctx, service, positional, flags, write) {
|
|
|
474
474
|
}
|
|
475
475
|
// 覆盖游标由 service 自动记录,无需传参
|
|
476
476
|
const ifRev = typeof flags['if-rev'] === 'string' ? parseInt(flags['if-rev'], 10) : undefined;
|
|
477
|
-
const r = service.setSummary(ctx.selfAid, sessionId, content, Number.isFinite(ifRev) ? ifRev : undefined);
|
|
477
|
+
const r = await service.setSummary(ctx.selfAid, sessionId, content, Number.isFinite(ifRev) ? ifRev : undefined);
|
|
478
478
|
const cursorNote = r.coveredThroughTs ? `,覆盖至 ${new Date(r.coveredThroughTs).toLocaleString('zh-CN')}` : '';
|
|
479
|
-
// --name
|
|
479
|
+
// --name:顺带改名。主会话(threadId 为空)同样可以改,走 renameThread 的主会话分支。
|
|
480
480
|
const rawName = flags.name.trim();
|
|
481
481
|
const newName = rawName && rawName !== '-' ? rawName : undefined;
|
|
482
|
-
if (newName
|
|
483
|
-
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})\n(主会话没有话题名,--name 已忽略)`);
|
|
484
|
-
}
|
|
485
|
-
if (newName && target.threadId) {
|
|
482
|
+
if (newName) {
|
|
486
483
|
try {
|
|
487
484
|
const renamed = await service.renameThread({
|
|
488
|
-
selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId, name: newName,
|
|
485
|
+
selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId ?? '', name: newName,
|
|
489
486
|
});
|
|
490
487
|
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})\n`
|
|
491
488
|
+ `✅ 话题已改名: ${renamed.name}`);
|
|
@@ -173,6 +173,20 @@ const CATALOG = [
|
|
|
173
173
|
{ path: 'newName', type: 'string' },
|
|
174
174
|
],
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
type: 'thread:renamed',
|
|
178
|
+
namespace: 'thread',
|
|
179
|
+
name: 'renamed',
|
|
180
|
+
description: '对端协议消息改写本端话题名',
|
|
181
|
+
fields: [
|
|
182
|
+
{ path: 'sessionId', type: 'string', optional: true },
|
|
183
|
+
{ path: 'peer', type: 'string' },
|
|
184
|
+
{ path: 'threadId', type: 'string' },
|
|
185
|
+
{ path: 'name', type: 'string' },
|
|
186
|
+
{ path: 'nameUpdatedAt', type: 'number' },
|
|
187
|
+
{ path: 'by', type: 'string' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
176
190
|
{
|
|
177
191
|
type: 'session:forked',
|
|
178
192
|
namespace: 'session',
|
|
@@ -125,8 +125,18 @@ export function registerDaemonIpc({ ipcServer, agentRegistry, agentDelegationReg
|
|
|
125
125
|
aid: params.from,
|
|
126
126
|
to: params.to,
|
|
127
127
|
scope: params.peerKind === 'group' ? 'group' : 'msg',
|
|
128
|
-
payload: {
|
|
128
|
+
payload: {
|
|
129
|
+
type: params.kind ?? 'topic-created',
|
|
130
|
+
topicName: params.topicName,
|
|
131
|
+
threadId: params.threadId,
|
|
132
|
+
...(params.nameUpdatedAt ? { nameUpdatedAt: params.nameUpdatedAt } : {}),
|
|
133
|
+
},
|
|
129
134
|
thread: params.threadId,
|
|
135
|
+
// 授权链需要托管上下文;宿主终端调用(localControl)没有 token,
|
|
136
|
+
// 此时 originSessionId 为空,sendAunMsg 会拒绝——按尽力而为处理。
|
|
137
|
+
...(params.originSessionId ? { originSessionId: params.originSessionId } : {}),
|
|
138
|
+
...(params.delegationToken ? { delegationToken: params.delegationToken } : {}),
|
|
139
|
+
...(params.delegationCommandHash ? { delegationCommandHash: params.delegationCommandHash } : {}),
|
|
130
140
|
});
|
|
131
141
|
if (!sent?.ok)
|
|
132
142
|
throw new Error(sent?.error || 'Topic notification failed');
|
|
@@ -95,6 +95,7 @@ export function updateBatchMeta(batchDir, updates) {
|
|
|
95
95
|
finishedAt: updates.finishedAt,
|
|
96
96
|
success: updates.success,
|
|
97
97
|
...(updates.agentSessionId ? { agentSessionId: updates.agentSessionId } : {}),
|
|
98
|
+
...(updates.failureReason ? { failureReason: updates.failureReason } : {}),
|
|
98
99
|
};
|
|
99
100
|
// 更新 feedback 正文
|
|
100
101
|
let newBody = body;
|
|
@@ -226,13 +226,50 @@ function fillCandidates(ctx, mode, forgetAfterDays) {
|
|
|
226
226
|
else if (mode === 'forget') {
|
|
227
227
|
const now = Date.now();
|
|
228
228
|
const thresholds = { forgetAfterDays };
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
// forget + scope=agent:扫描所有 relation 的 consolidated 目录,以及 agent 自身的 consolidated
|
|
230
|
+
if (ctx.scope === 'agent') {
|
|
231
|
+
ctx.candidates = [];
|
|
232
|
+
// ctx.consolidatedDir 是 agents/<selfAid>/personal/memory/consolidated,
|
|
233
|
+
// 往上走三级才是 agents/<selfAid>
|
|
234
|
+
const agentDir = path.dirname(path.dirname(path.dirname(ctx.consolidatedDir)));
|
|
235
|
+
const relationsRoot = path.join(agentDir, 'relations');
|
|
236
|
+
if (fs.existsSync(relationsRoot)) {
|
|
237
|
+
for (const peerKey of fs.readdirSync(relationsRoot)) {
|
|
238
|
+
const consolidatedDir = path.join(relationsRoot, peerKey, 'memory', 'consolidated');
|
|
239
|
+
if (fs.existsSync(consolidatedDir)) {
|
|
240
|
+
const candidates = collectCandidates(consolidatedDir, thresholds, now)
|
|
241
|
+
.filter(info => isDueForReview(info, thresholds, now))
|
|
242
|
+
.map(info => ({
|
|
243
|
+
relPath: `${peerKey}/consolidated/${info.name}`,
|
|
244
|
+
description: readFmValue(info.content, 'description') ?? info.name.replace(/\.md$/, ''),
|
|
245
|
+
tags: info.tags,
|
|
246
|
+
}));
|
|
247
|
+
ctx.candidates.push(...candidates);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// agent 自身的 consolidated(原 ⑤b 拉起 forget/agent 的来源)
|
|
252
|
+
if (fs.existsSync(ctx.consolidatedDir)) {
|
|
253
|
+
const own = collectCandidates(ctx.consolidatedDir, thresholds, now)
|
|
254
|
+
.filter(info => isDueForReview(info, thresholds, now))
|
|
255
|
+
.map(info => ({
|
|
256
|
+
relPath: `consolidated/${info.name}`,
|
|
257
|
+
description: readFmValue(info.content, 'description') ?? info.name.replace(/\.md$/, ''),
|
|
258
|
+
tags: info.tags,
|
|
259
|
+
}));
|
|
260
|
+
ctx.candidates.push(...own);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// relation / owner 级 forget:只扫描当前层级的 consolidated
|
|
265
|
+
ctx.candidates = collectCandidates(source, thresholds, now)
|
|
266
|
+
.filter(info => isDueForReview(info, thresholds, now))
|
|
267
|
+
.map(info => ({
|
|
268
|
+
relPath: `consolidated/${info.name}`,
|
|
269
|
+
description: readFmValue(info.content, 'description') ?? info.name.replace(/\.md$/, ''),
|
|
270
|
+
tags: info.tags,
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
236
273
|
}
|
|
237
274
|
}
|
|
238
275
|
// ── 渲染 ────────────────────────────────────────────────────────────────────
|
|
@@ -262,15 +299,45 @@ function renderScopeSection(ctx, mode, threadFiles) {
|
|
|
262
299
|
lines.push(`待整理的 note 在 \`${ctx.processingDir}\`(整理开始前已从 pending/ 整体移入,是本次的快照)。`);
|
|
263
300
|
}
|
|
264
301
|
else if (ctx.candidates) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
302
|
+
// forget + scope=agent:按 relation 分组展示
|
|
303
|
+
if (mode === 'forget' && ctx.scope === 'agent') {
|
|
304
|
+
const byRelation = new Map();
|
|
305
|
+
for (const c of ctx.candidates) {
|
|
306
|
+
const match = c.relPath.match(/^([^/\\]+)[/\\]consolidated[/\\]/);
|
|
307
|
+
const relation = match ? match[1] : 'unknown';
|
|
308
|
+
if (!byRelation.has(relation))
|
|
309
|
+
byRelation.set(relation, []);
|
|
310
|
+
byRelation.get(relation).push(c);
|
|
311
|
+
}
|
|
312
|
+
lines.push(`**候选记忆(共 ${ctx.candidates.length} 条,分布在 ${byRelation.size} 个关系)** —— 只列路径与描述,按需读正文:`);
|
|
313
|
+
lines.push('');
|
|
314
|
+
if (ctx.candidates.length === 0) {
|
|
315
|
+
lines.push('(无候选)');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
for (const [relation, candidates] of byRelation) {
|
|
319
|
+
lines.push(`#### relation: ${relation} (${candidates.length} 条)`);
|
|
320
|
+
lines.push('');
|
|
321
|
+
for (const c of candidates) {
|
|
322
|
+
const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
|
|
323
|
+
lines.push(`- \`${c.relPath}\`${tags} — ${c.description}`);
|
|
324
|
+
}
|
|
325
|
+
lines.push('');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
269
328
|
}
|
|
270
329
|
else {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
330
|
+
// 其他模式:直接列出候选
|
|
331
|
+
lines.push(`**候选记忆(${ctx.candidates.length} 条)** —— 只列路径与描述,按需读正文:`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
if (ctx.candidates.length === 0) {
|
|
334
|
+
lines.push('(无候选)');
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
for (const c of ctx.candidates) {
|
|
338
|
+
const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
|
|
339
|
+
lines.push(`- \`${c.relPath}\`${tags} — ${c.description}`);
|
|
340
|
+
}
|
|
274
341
|
}
|
|
275
342
|
}
|
|
276
343
|
}
|
|
@@ -494,6 +561,22 @@ export function serializeConsolidationContext(input) {
|
|
|
494
561
|
'',
|
|
495
562
|
...scopes.flatMap(ctx => [renderScopeSection(ctx, input.mode, threadFiles), '', '---', '']),
|
|
496
563
|
];
|
|
564
|
+
// 列出可读根(工具 path/file_path 参数必须落在这些路径内)
|
|
565
|
+
lines.push('', '**可读根(工具 path/file_path 参数必须落在这些路径内)**:');
|
|
566
|
+
lines.push('- 批次目录:`' + batchDir + '`');
|
|
567
|
+
for (const ctx of scopes) {
|
|
568
|
+
if (ctx.processingDir)
|
|
569
|
+
lines.push('- processingDir: `' + ctx.processingDir + '`');
|
|
570
|
+
if (ctx.consolidatedDir)
|
|
571
|
+
lines.push('- consolidatedDir: `' + ctx.consolidatedDir + '`');
|
|
572
|
+
if (ctx.indexPath)
|
|
573
|
+
lines.push('- indexPath: `' + ctx.indexPath + '`');
|
|
574
|
+
}
|
|
575
|
+
if (historyJsonlPath)
|
|
576
|
+
lines.push('- 消息流水: `' + historyJsonlPath + '`');
|
|
577
|
+
if (threadFiles && threadFiles.length > 0)
|
|
578
|
+
lines.push('- 话题快照: `' + path.join(batchDir, 'threads') + '`');
|
|
579
|
+
lines.push('', '---', '');
|
|
497
580
|
if (input.mode === 'notes' && threadFiles && threadFiles.length > 0) {
|
|
498
581
|
const threadsDir = path.join(batchDir, 'threads');
|
|
499
582
|
lines.push('## 话题快照文件', '');
|
|
@@ -379,19 +379,27 @@ export function checkConsolidationCandidates(params) {
|
|
|
379
379
|
out.push({ mode: 'profile', peerKey, reason: '索引有变化且画像超期' });
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
-
// ⑤ 超期记忆 → forget
|
|
382
|
+
// ⑤ 超期记忆 → forget(agent 级,扫描所有 relation + agent 自身,合并成一个会话)
|
|
383
|
+
let hasRelationForget = false;
|
|
383
384
|
for (const peerKey of peerKeys) {
|
|
384
385
|
const dir = path.join(relationMemoryDir(root, selfAid, peerKey), 'consolidated');
|
|
385
386
|
const due = checkStale(dir, now, config.forgetAfterDays);
|
|
386
|
-
if (due > 0)
|
|
387
|
-
|
|
388
|
-
}
|
|
387
|
+
if (due > 0)
|
|
388
|
+
hasRelationForget = true;
|
|
389
389
|
}
|
|
390
|
-
// ⑤b
|
|
390
|
+
// ⑤b agent 自身的 consolidated 超期
|
|
391
391
|
const agentConsolidated = path.join(root, 'agents', selfAid, 'personal', 'memory', 'consolidated');
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
const hasAgentForget = config.enableAgent && checkStale(agentConsolidated, now, config.forgetAfterDays) > 0;
|
|
393
|
+
// 只要有任意一层超期,起一个 forget scope=agent 会话,由会话内统一扫描
|
|
394
|
+
if (hasRelationForget || hasAgentForget) {
|
|
395
|
+
const parts = [];
|
|
396
|
+
if (hasRelationForget)
|
|
397
|
+
parts.push('关系级记忆超期');
|
|
398
|
+
if (hasAgentForget)
|
|
399
|
+
parts.push('agent 级记忆超期');
|
|
400
|
+
out.push({ mode: 'forget', scope: 'agent', reason: parts.join('、') });
|
|
394
401
|
}
|
|
402
|
+
// ⑤c owner 级 forget 保持独立(owner 层与 agent/relation 层是平行的)
|
|
395
403
|
if (config.enableOwner && ownerAid) {
|
|
396
404
|
const ownerConsolidated = path.join(root, 'memory', 'owner', ownerAid, 'consolidated');
|
|
397
405
|
if (checkStale(ownerConsolidated, now, config.forgetAfterDays) > 0) {
|
|
@@ -13,6 +13,7 @@ import path from 'path';
|
|
|
13
13
|
import { agentDir, resolveRoot } from '../../paths.js';
|
|
14
14
|
import { serializeConsolidationContext } from './consolidation-context.js';
|
|
15
15
|
import { assertSystemSessionLaunchable } from '../system-session/registry.js';
|
|
16
|
+
import { resolveAgentLifecycle } from '../../config/lifecycle.js';
|
|
16
17
|
import { ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
17
18
|
import { formatChannelKey } from '../channel-loader.js';
|
|
18
19
|
import { logger } from '../../utils/logger.js';
|
|
@@ -48,6 +49,21 @@ function cleanupPreviousConsolidationBatches(_selfAid) {
|
|
|
48
49
|
logger.warn(`[Memory] 清理整理批次时出错: ${e?.message}`);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 首次设定(lifecycle)没完成的 agent 不能整理记忆:整理会话会被注入首次设定规则而拒绝干活,
|
|
54
|
+
* note 却照样被认领。返回拒绝原因;可以整理时返回 undefined。
|
|
55
|
+
* 直接读磁盘上的 config.json:首次设定完成时 lifecycle 就落在这里,不依赖内存里的注册表。
|
|
56
|
+
*/
|
|
57
|
+
export function consolidationBlockedReason(selfAid) {
|
|
58
|
+
const configPath = path.join(resolveRoot(), 'agents', selfAid, 'config.json');
|
|
59
|
+
const config = fs.existsSync(configPath)
|
|
60
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
61
|
+
: {};
|
|
62
|
+
const lifecycle = resolveAgentLifecycle(config);
|
|
63
|
+
if (lifecycle === 'active')
|
|
64
|
+
return undefined;
|
|
65
|
+
return `agent ${selfAid} 首次设定尚未完成(lifecycle=${lifecycle ?? String(config.lifecycle)}),完成后才能整理记忆`;
|
|
66
|
+
}
|
|
51
67
|
/**
|
|
52
68
|
* 序列化上下文并直接创建整理会话。
|
|
53
69
|
* 失败抛错,调用方负责决定是否记录诊断或回传错误。
|
|
@@ -64,6 +80,9 @@ export async function launchConsolidation(channel, params) {
|
|
|
64
80
|
// 放在认领 processing 之前,拒绝时不留副作用。
|
|
65
81
|
const systemSession = { type: 'memory', subtype: mode };
|
|
66
82
|
assertSystemSessionLaunchable(systemSession, effectiveCtx.baseagent ?? 'claude');
|
|
83
|
+
const blockedReason = consolidationBlockedReason(selfAid);
|
|
84
|
+
if (blockedReason)
|
|
85
|
+
throw new Error(blockedReason);
|
|
67
86
|
// 清理旧的整理批次(保留最新的一个历史)
|
|
68
87
|
cleanupPreviousConsolidationBatches(selfAid);
|
|
69
88
|
// notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
|
|
@@ -213,6 +232,20 @@ function moveIntoDir(srcPath, destDir, stamp) {
|
|
|
213
232
|
}
|
|
214
233
|
fs.renameSync(srcPath, dest);
|
|
215
234
|
}
|
|
235
|
+
/** 本次整理认领到 processing/ 的 note 条数(收尾归位之前调用)。 */
|
|
236
|
+
function countClaimedNotes(memoryDir) {
|
|
237
|
+
if (!memoryDir)
|
|
238
|
+
return 0;
|
|
239
|
+
const processingDir = path.join(memoryDir, 'processing');
|
|
240
|
+
try {
|
|
241
|
+
return fs.readdirSync(processingDir)
|
|
242
|
+
.filter(entry => entry.endsWith('.md') && fs.statSync(path.join(processingDir, entry)).isFile())
|
|
243
|
+
.length;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
216
249
|
function restorePendingNotes(memoryDir) {
|
|
217
250
|
const processingDir = path.join(memoryDir, 'processing');
|
|
218
251
|
if (!fs.existsSync(processingDir))
|
|
@@ -363,6 +396,17 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
|
|
|
363
396
|
finalizedSessions.add(sessionId);
|
|
364
397
|
releaseAid = session.selfAID;
|
|
365
398
|
const { summarizeOpsRecords, collectAffectedScopes } = await import('./consolidation-ops-records.js');
|
|
399
|
+
// 会话正常结束不等于整理做完了。notes 模式认领了 note,却既没写记忆也没写反馈,
|
|
400
|
+
// 说明 agent 没处理这批 note(例如拒绝了任务)——按失败收尾,note 回 pending 下次重试。
|
|
401
|
+
let failureReason;
|
|
402
|
+
if (success && meta.memoryMode === 'notes' && countClaimedNotes(meta.consolidationMemoryDir) > 0) {
|
|
403
|
+
const ops = summarizeOpsRecords(batchDir);
|
|
404
|
+
if (ops.created + ops.updated + ops.archived === 0 && !readFeedbackText(batchDir)) {
|
|
405
|
+
success = false;
|
|
406
|
+
failureReason = '整理会话结束时没有写入任何记忆,也没有写反馈,note 未被处理';
|
|
407
|
+
logger.warn(`[Memory] ${failureReason}(session=${sessionId}),按失败收尾`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
366
410
|
if (!success) {
|
|
367
411
|
await rollbackConsolidation(batchDir, session.selfAID);
|
|
368
412
|
}
|
|
@@ -478,6 +522,7 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
|
|
|
478
522
|
finishedAt: new Date().toISOString(),
|
|
479
523
|
success,
|
|
480
524
|
...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
|
|
525
|
+
...(failureReason ? { failureReason } : {}),
|
|
481
526
|
feedbackText: customText,
|
|
482
527
|
});
|
|
483
528
|
}
|
|
@@ -497,6 +542,7 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
|
|
|
497
542
|
success,
|
|
498
543
|
summary: summarizeOpsRecords(batchDir),
|
|
499
544
|
batchDir,
|
|
545
|
+
failureReason,
|
|
500
546
|
});
|
|
501
547
|
await sendFeedback(feedbackOrigin, text, session.selfAID);
|
|
502
548
|
}
|
|
@@ -738,6 +784,10 @@ export function registerConsolidationLaunchExecutor({ ipcServer, enqueueConsolid
|
|
|
738
784
|
ipcServer.setConsolidationLaunchExecutor(async (params) => {
|
|
739
785
|
if (!params.taskCtx.selfAid)
|
|
740
786
|
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
787
|
+
// 入队前就拦:拉起失败在队列里只会记成一句笼统的「拉起整理会话失败」
|
|
788
|
+
const blockedReason = consolidationBlockedReason(params.taskCtx.selfAid);
|
|
789
|
+
if (blockedReason)
|
|
790
|
+
throw new Error(blockedReason);
|
|
741
791
|
const launched = await enqueueConsolidation(params);
|
|
742
792
|
if (!launched)
|
|
743
793
|
throw new Error('拉起整理会话失败,详见 daemon 日志');
|
|
@@ -752,6 +802,8 @@ export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, e
|
|
|
752
802
|
const agentConfig = agentRegistry.get(selfAid)?.config;
|
|
753
803
|
if (agentConfig?.memory?.enableRelation === false)
|
|
754
804
|
return { launched: [] };
|
|
805
|
+
if (consolidationBlockedReason(selfAid))
|
|
806
|
+
return { launched: [] };
|
|
755
807
|
const ownerAid = agentConfig?.owners?.[0];
|
|
756
808
|
const agentCount = agentRegistry.runnableAgents().length;
|
|
757
809
|
const relationsDir = path.join(agentDir(selfAid), 'relations');
|
|
@@ -808,6 +860,8 @@ export function registerRelationConsolidationEvents({ eventBus, agentRegistry, r
|
|
|
808
860
|
return;
|
|
809
861
|
if (taskCtx.sessionType === 'memory')
|
|
810
862
|
return;
|
|
863
|
+
if (consolidationBlockedReason(taskCtx.selfAid))
|
|
864
|
+
return;
|
|
811
865
|
const memoryConfig = agentRegistry.get(taskCtx.selfAid)?.config?.memory;
|
|
812
866
|
if (memoryConfig?.enableRelation === false)
|
|
813
867
|
return;
|
|
@@ -872,6 +926,7 @@ export function renderDefaultConsolidationFeedback(input) {
|
|
|
872
926
|
'',
|
|
873
927
|
`**模式**:${mode}`,
|
|
874
928
|
`**触发**:${cause}`,
|
|
929
|
+
...(input.failureReason ? [`**原因**:${input.failureReason}`] : []),
|
|
875
930
|
'',
|
|
876
931
|
'本次整理未完成,已回退未归档的改动。详情见整理批次:',
|
|
877
932
|
`\`${input.batchDir}\``,
|
|
@@ -50,8 +50,10 @@ export function resolveScopeDirs(scope, taskCtx, ownerAid, peerKeyOverride) {
|
|
|
50
50
|
/**
|
|
51
51
|
* 把用户给的路径解析成 consolidated/ 下的绝对路径。
|
|
52
52
|
* 允许 slug / <slug>.md / 相对或绝对路径(须落在本作用域的 consolidated/ 内)。
|
|
53
|
+
*
|
|
54
|
+
* forget-agent 模式特殊处理:允许路径落在任意 relation 的 consolidated/ 下。
|
|
53
55
|
*/
|
|
54
|
-
export function resolveConsolidatedPath(dirs, input) {
|
|
56
|
+
export function resolveConsolidatedPath(dirs, input, taskCtx) {
|
|
55
57
|
const base = path.resolve(dirs.consolidatedDir);
|
|
56
58
|
const looksLikePath = input.includes('/') || input.includes('\\');
|
|
57
59
|
if (!looksLikePath) {
|
|
@@ -66,6 +68,20 @@ export function resolveConsolidatedPath(dirs, input) {
|
|
|
66
68
|
if (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
67
69
|
return abs;
|
|
68
70
|
}
|
|
71
|
+
// forget-agent 模式:允许路径落在任意 relation 的 consolidated/ 下,或 agent 自身的 consolidated/
|
|
72
|
+
const isForgetAgent = taskCtx?.memoryMode === 'forget' && taskCtx?.memoryScope === 'agent';
|
|
73
|
+
if (isForgetAgent && taskCtx?.selfAid) {
|
|
74
|
+
const relationsRoot = path.join(resolveRoot(), 'agents', taskCtx.selfAid, 'relations');
|
|
75
|
+
const absInput = path.resolve(input);
|
|
76
|
+
const relToRelations = path.relative(relationsRoot, absInput);
|
|
77
|
+
// 检查是否在某个 relation 的 consolidated/ 下
|
|
78
|
+
if (!relToRelations.startsWith('..') && !path.isAbsolute(relToRelations)) {
|
|
79
|
+
const parts = relToRelations.split(path.sep);
|
|
80
|
+
if (parts.length >= 3 && parts[1] === 'memory' && parts[2] === 'consolidated') {
|
|
81
|
+
return absInput;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
throw new Error(`ROLE_ACCESS_DENIED: 只能操作本作用域 consolidated/ 下的记忆(${base}),当前路径越界: ${input}`);
|
|
70
86
|
}
|
|
71
87
|
function assertSlug(slug) {
|
|
@@ -104,7 +120,7 @@ export async function createMemory(req) {
|
|
|
104
120
|
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid, req.targetPeerKey);
|
|
105
121
|
fs.mkdirSync(dirs.consolidatedDir, { recursive: true });
|
|
106
122
|
const targetPath = path.join(dirs.consolidatedDir, `${req.slug}.md`);
|
|
107
|
-
const replacePaths = (req.replaces ?? []).map(p => resolveConsolidatedPath(dirs, p));
|
|
123
|
+
const replacePaths = (req.replaces ?? []).map(p => resolveConsolidatedPath(dirs, p, req.taskCtx));
|
|
108
124
|
// 撞名闸门:目标已存在且没被 --replaces 覆盖 → 拒绝
|
|
109
125
|
if (fs.existsSync(targetPath) && !replacePaths.includes(targetPath)) {
|
|
110
126
|
throw new Error(`SLUG_EXISTS: 目标已存在: ${targetPath}\n` +
|
|
@@ -264,7 +280,7 @@ export async function demoteMemory(req) {
|
|
|
264
280
|
}
|
|
265
281
|
export async function updateMemory(req) {
|
|
266
282
|
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid);
|
|
267
|
-
const filePath = resolveConsolidatedPath(dirs, req.target);
|
|
283
|
+
const filePath = resolveConsolidatedPath(dirs, req.target, req.taskCtx);
|
|
268
284
|
if (!fs.existsSync(filePath))
|
|
269
285
|
throw new Error(`找不到记忆文件: ${filePath}`);
|
|
270
286
|
const document = readMemory(readVerifiedMemory(filePath));
|
|
@@ -286,7 +302,7 @@ export async function archiveMemory(req) {
|
|
|
286
302
|
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid);
|
|
287
303
|
const archived = [];
|
|
288
304
|
for (const t of req.targets) {
|
|
289
|
-
const p = resolveConsolidatedPath(dirs, t);
|
|
305
|
+
const p = resolveConsolidatedPath(dirs, t, req.taskCtx);
|
|
290
306
|
if (!fs.existsSync(p))
|
|
291
307
|
throw new Error(`找不到记忆文件: ${p}`);
|
|
292
308
|
const dest = archivePath(dirs.memoryDir, p);
|
|
@@ -28,7 +28,7 @@ export function createAunHandoffSender(sessionManager, channelInstances) {
|
|
|
28
28
|
const log = {
|
|
29
29
|
content: classified.content,
|
|
30
30
|
sessionId: targetSession.id,
|
|
31
|
-
threadId: targetSession.threadId ||
|
|
31
|
+
threadId: targetSession.threadId || '',
|
|
32
32
|
msgType: classified.msgType,
|
|
33
33
|
payloadType: classified.payloadType,
|
|
34
34
|
payloadSummary: classified.payloadSummary,
|
|
@@ -267,6 +267,24 @@ export function registerAunIpcSender({ ipcServer, channelInstances, agentDelegat
|
|
|
267
267
|
if (!payload) {
|
|
268
268
|
return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
|
|
269
269
|
}
|
|
270
|
+
// 托管 argv 通道(runManagedSend)先前不带 log 元数据:群聊 sendDaemonGroupMsg
|
|
271
|
+
// 自身会写日志,私聊 sendDaemonMsg 只在 args.log 存在时才写,于是同一份
|
|
272
|
+
// `ec msg send` 在私聊下不落 messages.jsonl,话题历史里查不到已发出的消息。
|
|
273
|
+
// 这里在 payload 定型后按同一套规则补出日志元数据,让两条路径记账一致。
|
|
274
|
+
if (params.argv !== undefined && params.log === undefined) {
|
|
275
|
+
const classified = classifyAunPayloadForLog(payload);
|
|
276
|
+
params = {
|
|
277
|
+
...params,
|
|
278
|
+
log: {
|
|
279
|
+
content: classified.content,
|
|
280
|
+
threadId: params.thread ?? '',
|
|
281
|
+
msgType: classified.msgType,
|
|
282
|
+
payloadType: classified.payloadType,
|
|
283
|
+
payloadSummary: classified.payloadSummary,
|
|
284
|
+
source: 'msg',
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
270
288
|
try {
|
|
271
289
|
if (Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
|
|
272
290
|
payload = { ...payload, mentions: normalizeAunMentionEntries(payload.mentions) };
|
|
@@ -1264,7 +1264,7 @@ export class MessageBridge {
|
|
|
1264
1264
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
1265
1265
|
from: msg.peerId || 'unknown',
|
|
1266
1266
|
to: msg.selfAID || 'self',
|
|
1267
|
-
threadId: msg.threadId ??
|
|
1267
|
+
threadId: msg.threadId ?? '',
|
|
1268
1268
|
chatType: msg.chatType || 'private',
|
|
1269
1269
|
groupId: msg.groupId ?? null,
|
|
1270
1270
|
msgId: msg.messageId ?? null,
|
|
@@ -1549,7 +1549,7 @@ export class MessageBridge {
|
|
|
1549
1549
|
from: msg.peerId || 'unknown',
|
|
1550
1550
|
to: msg.selfAID || 'self',
|
|
1551
1551
|
sessionId: session.id,
|
|
1552
|
-
threadId: session.threadId ||
|
|
1552
|
+
threadId: session.threadId || '',
|
|
1553
1553
|
chatType,
|
|
1554
1554
|
groupId: msg.groupId ?? null,
|
|
1555
1555
|
msgId: msg.messageId ?? null,
|
|
@@ -1608,7 +1608,7 @@ export class MessageBridge {
|
|
|
1608
1608
|
const member = item.value;
|
|
1609
1609
|
appendMessageLog(inboundEntry.chatDir, buildInboundEntry({
|
|
1610
1610
|
from: item.sender, to: selfAid || '', sessionId: session.id,
|
|
1611
|
-
threadId: session.threadId ||
|
|
1611
|
+
threadId: session.threadId || '', chatType: 'group', groupId: msg.groupId,
|
|
1612
1612
|
msgId: item.messageId, content: member.messageLogContent ?? item.content,
|
|
1613
1613
|
permMode: item.role, timestamp: item.receivedAt,
|
|
1614
1614
|
receivedAt: member.receivedAt, gatewaySeq: member.gatewaySeq,
|
|
@@ -31,6 +31,23 @@ export function resolveChatMode(params) {
|
|
|
31
31
|
? effectiveValue
|
|
32
32
|
: chatmodeFactoryDefaults()[field];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* 出站发送路径(`ec msg send` / daemon 私聊发送)用的最小判定。
|
|
36
|
+
*
|
|
37
|
+
* 这两条路径只有对端身份、拿不到角色,因此不能复用 `resolveChatMode` 的
|
|
38
|
+
* 分层配置。口径与 `resolveChatMode` 的硬约束保持一致:
|
|
39
|
+
* - 非人类对端(isMachine)→ proactive
|
|
40
|
+
* - 人类但角色未知 → proactive(保守侧)
|
|
41
|
+
* - 人类且 owner/admin → interactive
|
|
42
|
+
*
|
|
43
|
+
* 角色未知时不能假设高信任:interactive 表示"输出即回复"的同步交互,
|
|
44
|
+
* 只应给已确认的管理者。
|
|
45
|
+
*/
|
|
46
|
+
export function chatmodeForPeerIdentity(identity, role) {
|
|
47
|
+
if (identity.isMachine)
|
|
48
|
+
return 'proactive';
|
|
49
|
+
return resolveChatMode({ role: role ?? undefined, chatType: 'private', peerType: identity.type ?? 'human' });
|
|
50
|
+
}
|
|
34
51
|
export function resolveChatModeForField(params) {
|
|
35
52
|
return resolveChatMode({
|
|
36
53
|
...params,
|
|
@@ -933,8 +933,18 @@ function wasRetryInputAlreadySubmitted(error) {
|
|
|
933
933
|
return !!(error && typeof error === 'object' && error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] === true);
|
|
934
934
|
}
|
|
935
935
|
function formatRetryInputAlreadySubmittedMessage(error) {
|
|
936
|
-
const
|
|
937
|
-
|
|
936
|
+
const raw = getErrorRawText(error);
|
|
937
|
+
const record = error && typeof error === 'object' ? error : undefined;
|
|
938
|
+
const retryCount = typeof record?.codexRetryCount === 'number' && Number.isSafeInteger(record.codexRetryCount)
|
|
939
|
+
? record.codexRetryCount : undefined;
|
|
940
|
+
const statusCode = raw?.match(/\b([45]\d{2})\b/)?.[1];
|
|
941
|
+
if (retryCount !== undefined && statusCode) {
|
|
942
|
+
return `⚠️ Codex 已重连 ${retryCount} 次模型服务后仍失败(${statusCode})。本次执行已停止。`;
|
|
943
|
+
}
|
|
944
|
+
if (retryCount !== undefined) {
|
|
945
|
+
return `⚠️ Codex 已重连 ${retryCount} 次模型服务后仍失败。本次执行已停止。`;
|
|
946
|
+
}
|
|
947
|
+
return '⚠️ Codex 已接收本次消息,但模型服务请求失败。本次执行已停止。';
|
|
938
948
|
}
|
|
939
949
|
function isNonErrorInterruptReason(reason) {
|
|
940
950
|
return reason === 'new_message'
|
|
@@ -4660,7 +4670,8 @@ export class ResponseEngine {
|
|
|
4660
4670
|
peerId: peerIdRaw || undefined,
|
|
4661
4671
|
peerKey,
|
|
4662
4672
|
peerName: peerName || undefined,
|
|
4663
|
-
peerRole
|
|
4673
|
+
// 系统会话与角色体系无关:不给 peerRole,消息头就不渲染 `· role:` 段
|
|
4674
|
+
peerRole: systemSessionMarker ? undefined : peerRole,
|
|
4664
4675
|
// Display missing identity explicitly without changing the mode resolver's legacy fallback.
|
|
4665
4676
|
peerType: peerType || 'unknown',
|
|
4666
4677
|
sameDevice: message.sameDevice ?? false,
|
|
@@ -4794,7 +4805,7 @@ export class ResponseEngine {
|
|
|
4794
4805
|
} : {}),
|
|
4795
4806
|
peerId: message.peerId, peerName: peerName || undefined,
|
|
4796
4807
|
peerType: message.peerType,
|
|
4797
|
-
peerRole: message.batchRole || session.identity?.role || 'none',
|
|
4808
|
+
peerRole: systemSessionMarker ? undefined : (message.batchRole || session.identity?.role || 'none'),
|
|
4798
4809
|
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
4799
4810
|
encrypted: message.encrypted,
|
|
4800
4811
|
content: message.content, timestamp: message.timestamp,
|