evolcore 0.0.39 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +51 -27
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -1,23 +1,20 @@
1
1
  /**
2
- * consolidation-trigger.ts — 整理候选检查(v4,纯函数)
2
+ * consolidation-trigger.ts — 整理候选判据(v5,纯函数)
3
3
  *
4
- * 职责只有一个:**检查五类候选,返回该拉起哪些整理会话。**
4
+ * 整理一律以关系为单位。两类判据:
5
5
  *
6
- * ① pending 堆积超过 backlogHours 的关系 → notes 模式(无会话历史,凭 note 内容整理)
7
- * ② 带 to-generalize 的记忆 → generalize 模式
8
- * ③ 带 to-promote 的记忆 → promote 模式
9
- * ④ 索引有变化且距上次画像超过 profileRegenDays → profile 模式
10
- * ⑤ 有记忆 mtime 超过 forgetAfterDays → forget 模式
6
+ * **关系级**(`isRelationModeDue`,供关系整理链逐步调用,见 relation-sweep.ts):
7
+ * notes pending/ 里有 note
8
+ * generalize 本关系有 to-generalize
9
+ * promote 本关系是 owner 关系且有 to-promote
10
+ * profile 索引有变化且距上次画像超过 profileRegenDays(或还没有画像)
11
+ * forget 本关系有超期记忆
11
12
  *
12
- * 判据是**数文件和数标签**,不看轮次和工具调用次数——活干得多不代表写得出记忆,
13
- * 只干两轮也可能产出五条要整理的内容。
13
+ * **全局定时检查**(`checkConsolidationCandidates`,cron script 驱动):只管不属于任何关系的部分——
14
+ * agent 层 / owner 层 forget,外加 pending 堆积超过 backlogHours 的兜底。
14
15
  *
15
- * v4 变化:不再自己实现定时器(原 setInterval 类已删除)。定时由 evolcore trigger 的
16
- * cron `execution.type='script'` 驱动,脚本调 daemon 的 `memory.check-consolidation`,
17
- * daemon 侧调用本模块的 `checkConsolidationCandidates()`,结果经 enqueueConsolidation 入队。
18
- *
19
- * 因此本模块**不持有任何进程内状态**(无 lastTaskEndAt 空闲门控)——所有判据都读磁盘。
20
- * 并发安全由 daemon 侧的整理队列保证(同 agent 串行)。
16
+ * 判据是**数文件和数标签**,不看轮次和工具调用次数。本模块不持有进程内状态,判据都读磁盘;
17
+ * 关系的空闲 / 兜底计时在 relation-idle-monitor.ts。
21
18
  */
22
19
  import fs from 'fs';
23
20
  import path from 'path';
@@ -321,22 +318,70 @@ function checkStale(consolidatedDir, now, forgetAfterDays) {
321
318
  export function shouldUseMetaIndex(agentCount, relationCount) {
322
319
  return agentCount * relationCount > META_INDEX_THRESHOLD;
323
320
  }
321
+ /** 关系整理链的固定顺序。每步在上一步收尾后重新判定,notes 新产出的标签要被后面几步看到。 */
322
+ export const RELATION_SWEEP_ORDER = ['notes', 'generalize', 'promote', 'profile', 'forget'];
323
+ /** pending/ 里有没有待整理的 note。 */
324
+ function hasPendingNotes(pendingDir) {
325
+ if (!fs.existsSync(pendingDir))
326
+ return false;
327
+ try {
328
+ return fs.readdirSync(pendingDir).some(f => f.endsWith('.md'));
329
+ }
330
+ catch {
331
+ return false;
332
+ }
333
+ }
334
+ /** 默认的关系空闲时长与兜底时长(小时)。 */
335
+ export const DEFAULT_RELATION_IDLE_HOURS = 1;
336
+ export const DEFAULT_MAX_UNCONSOLIDATED_HOURS = 8;
324
337
  /**
325
- * 检查五类候选,返回该拉起的整理会话列表。
338
+ * 单个关系的某个整理模式现在该不该跑。关系整理链逐步调用。
326
339
  *
327
- * 纯检查 + 少量副作用(forget 的长期记忆减速刷新 mtime)。
328
- * 调用方负责把结果入队(见 index.ts 的 enqueueConsolidation)。
340
+ * 与全局检查不同:notes 只要有 pending 就跑(关系已空闲 / 兜底到期,不再等堆积),
341
+ * promote 只在该关系就是 owner 关系时才可能命中。
342
+ */
343
+ export function isRelationModeDue(params) {
344
+ const { mode, selfAid, peerKey } = params;
345
+ const now = params.now ?? Date.now();
346
+ const root = params.root ?? resolveRoot();
347
+ const config = effectiveConfig(params.config);
348
+ const useMetaIndex = params.useMetaIndex ?? false;
349
+ if (!config.enableRelation)
350
+ return false;
351
+ const memDir = relationMemoryDir(root, selfAid, peerKey);
352
+ switch (mode) {
353
+ case 'notes':
354
+ return hasPendingNotes(path.join(memDir, 'pending'));
355
+ case 'generalize':
356
+ return config.enableAgent && !!countTagged(root, selfAid, [peerKey], 'to-generalize', useMetaIndex);
357
+ case 'promote':
358
+ return config.enableOwner
359
+ && findOwnerPeerKey([peerKey], params.ownerAid) === peerKey
360
+ && !!countTagged(root, selfAid, [peerKey], 'to-promote', useMetaIndex);
361
+ case 'profile':
362
+ return profileDueWithMeta(memDir, now, config.profileRegenDays, useMetaIndex);
363
+ case 'forget':
364
+ return checkStale(path.join(memDir, 'consolidated'), now, config.forgetAfterDays) > 0;
365
+ }
366
+ }
367
+ /**
368
+ * 全局定时检查:只管**不属于任何一个关系**的部分,外加 pending 堆积兜底。
369
+ *
370
+ * ① pending 堆积超过 backlogHours 的关系 → notes(由调用方转成该关系的整理链)
371
+ * ② agent 层超期记忆(只看 personal/memory 自身)→ forget scope=agent
372
+ * ③ owner 层超期记忆 → forget scope=owner
329
373
  *
330
- * 两种扫描路径:
331
- * - useMetaIndex=false(默认):读每个 .md 全文数标签 / 找画像(全量扫描)
332
- * - useMetaIndex=true:先读 `_meta.json` 汇总;缺失或不符时该作用域回退全量扫描
374
+ * generalize / promote / profile / 关系层 forget 由关系整理链负责(relation-sweep.ts),
375
+ * 这里不再产出:整理一律以关系为单位。
376
+ *
377
+ * 纯检查 + 少量副作用(forget 的长期记忆减速刷新 mtime)。
378
+ * useMetaIndex 参数保留兼容:当前三类判据都不读 `_meta.json`(关系级判据见 isRelationModeDue)。
333
379
  */
334
380
  export function checkConsolidationCandidates(params) {
335
381
  const { selfAid, ownerAid } = params;
336
382
  const now = params.now ?? Date.now();
337
383
  const root = params.root ?? resolveRoot();
338
384
  const config = effectiveConfig(params.config);
339
- const useMetaIndex = params.useMetaIndex ?? false;
340
385
  const out = [];
341
386
  if (!config.enableRelation)
342
387
  return out;
@@ -356,50 +401,12 @@ export function checkConsolidationCandidates(params) {
356
401
  });
357
402
  }
358
403
  }
359
- // ② 待泛化 → generalize(扫所有关系)
360
- if (config.enableAgent) {
361
- const hit = countTagged(root, selfAid, peerKeys, 'to-generalize', useMetaIndex);
362
- if (hit)
363
- out.push({ mode: 'generalize', reason: `发现 ${hit.count} 条 to-generalize` });
364
- }
365
- // ③ 待提升 → promote(只看 owner 关系)
366
- if (config.enableOwner && ownerAid) {
367
- const ownerPeerKey = findOwnerPeerKey(peerKeys, ownerAid);
368
- if (ownerPeerKey) {
369
- const hit = countTagged(root, selfAid, [ownerPeerKey], 'to-promote', useMetaIndex);
370
- if (hit) {
371
- out.push({ mode: 'promote', peerKey: ownerPeerKey, reason: `发现 ${hit.count} 条 to-promote` });
372
- }
373
- }
374
- }
375
- // ④ 画像超期 → profile
376
- for (const peerKey of peerKeys) {
377
- const memDir = relationMemoryDir(root, selfAid, peerKey);
378
- if (profileDueWithMeta(memDir, now, config.profileRegenDays, useMetaIndex)) {
379
- out.push({ mode: 'profile', peerKey, reason: '索引有变化且画像超期' });
380
- }
381
- }
382
- // ⑤ 超期记忆 → forget(agent 级,扫描所有 relation + agent 自身,合并成一个会话)
383
- let hasRelationForget = false;
384
- for (const peerKey of peerKeys) {
385
- const dir = path.join(relationMemoryDir(root, selfAid, peerKey), 'consolidated');
386
- const due = checkStale(dir, now, config.forgetAfterDays);
387
- if (due > 0)
388
- hasRelationForget = true;
389
- }
390
- // ⑤b agent 自身的 consolidated 超期
404
+ // ② agent 层超期 → forget scope=agent(只审 personal/memory 自身;关系层由关系整理链负责)
391
405
  const agentConsolidated = path.join(root, 'agents', selfAid, 'personal', 'memory', 'consolidated');
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('、') });
406
+ if (config.enableAgent && checkStale(agentConsolidated, now, config.forgetAfterDays) > 0) {
407
+ out.push({ mode: 'forget', scope: 'agent', reason: 'agent 级记忆超期' });
401
408
  }
402
- // ⑤c owner 级 forget 保持独立(owner 层与 agent/relation 层是平行的)
409
+ // ③ owner 层超期 → forget scope=owner(owner 层与 agent / relation 层是平行的)
403
410
  if (config.enableOwner && ownerAid) {
404
411
  const ownerConsolidated = path.join(root, 'memory', 'owner', ownerAid, 'consolidated');
405
412
  if (checkStale(ownerConsolidated, now, config.forgetAfterDays) > 0) {
@@ -12,13 +12,14 @@ import fs from 'fs';
12
12
  import { getPrimaryOwner, ownerResolver } from '../ownership/owner-resolver.js';
13
13
  import path from 'path';
14
14
  import { agentDir, resolveRoot } from '../../paths.js';
15
- import { serializeConsolidationContext } from './consolidation-context.js';
15
+ import { isSilentConsolidationTrigger, serializeConsolidationContext } from './consolidation-context.js';
16
16
  import { assertSystemSessionLaunchable } from '../system-session/registry.js';
17
17
  import { resolveAgentLifecycle } from '../../config/lifecycle.js';
18
18
  import { ensureSessionRuntimeDir } from '../../cli/task-context.js';
19
19
  import { formatChannelKey } from '../channel-loader.js';
20
20
  import { isTopicThreadId } from '../thread/topic-id.js';
21
21
  import { logger } from '../../utils/logger.js';
22
+ import { atomicWriteText } from '../../utils/atomic-write.js';
22
23
  import { appendOpsRecord, feedbackFilePath, readFeedbackText } from './consolidation-ops-records.js';
23
24
  import { updateBatchMeta } from './batch-meta.js';
24
25
  import { checkConsolidationCandidates, shouldUseMetaIndex } from './consolidation-trigger.js';
@@ -51,6 +52,54 @@ function cleanupPreviousConsolidationBatches(_selfAid) {
51
52
  logger.warn(`[Memory] 清理整理批次时出错: ${e?.message}`);
52
53
  }
53
54
  }
55
+ const PROCESSING_LEASE_FILENAME = '.processing-lease.json';
56
+ export const PROCESSING_LEASE_TIMEOUT_MS = 2 * 60 * 60_000;
57
+ function processingLeasePath(memoryDir) {
58
+ return path.join(memoryDir, PROCESSING_LEASE_FILENAME);
59
+ }
60
+ function writeProcessingLease(memoryDir, lease) {
61
+ atomicWriteText(processingLeasePath(memoryDir), JSON.stringify(lease) + '\n');
62
+ }
63
+ function readProcessingLease(memoryDir) {
64
+ const filePath = processingLeasePath(memoryDir);
65
+ let stat;
66
+ try {
67
+ stat = fs.lstatSync(filePath);
68
+ }
69
+ catch (error) {
70
+ if (error.code === 'ENOENT')
71
+ return undefined;
72
+ throw error;
73
+ }
74
+ if (!stat.isFile())
75
+ throw new Error(`记忆整理 processing 租约不是普通文件: ${filePath}`);
76
+ let value;
77
+ try {
78
+ value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
79
+ }
80
+ catch {
81
+ throw new Error(`记忆整理 processing 租约损坏: ${filePath}`);
82
+ }
83
+ if (!value || typeof value !== 'object'
84
+ || value.version !== 1
85
+ || !Number.isSafeInteger(value.claimedAt)
86
+ || value.claimedAt < 0) {
87
+ throw new Error(`记忆整理 processing 租约格式无效: ${filePath}`);
88
+ }
89
+ return value;
90
+ }
91
+ function removeProcessingLease(memoryDir) {
92
+ const filePath = processingLeasePath(memoryDir);
93
+ fs.rmSync(filePath, { force: true });
94
+ try {
95
+ for (const entry of fs.readdirSync(memoryDir)) {
96
+ if (entry.startsWith(`${PROCESSING_LEASE_FILENAME}.tmp.`)) {
97
+ fs.rmSync(path.join(memoryDir, entry), { force: true });
98
+ }
99
+ }
100
+ }
101
+ catch { /* The memory directory may already have been removed. */ }
102
+ }
54
103
  /**
55
104
  * 首次设定(lifecycle)没完成的 agent 不能整理记忆:整理会话会被注入首次设定规则而拒绝干活,
56
105
  * note 却照样被认领。返回拒绝原因;可以整理时返回 undefined。
@@ -93,27 +142,39 @@ export async function launchConsolidation(channel, params) {
93
142
  // notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
94
143
  let memoryDir;
95
144
  let claimedProcessing = false;
145
+ let processingClaimedAt;
96
146
  if (mode === 'notes') {
97
147
  memoryDir = effectiveCtx.peerKey
98
148
  ? path.join(resolveRoot(), 'agents', selfAid, 'relations', effectiveCtx.peerKey, 'memory')
99
149
  : path.join(resolveRoot(), 'agents', selfAid, 'personal', 'memory');
100
150
  const pendingDir = path.join(memoryDir, 'pending');
101
151
  const processingDir = path.join(memoryDir, 'processing');
152
+ if (fs.existsSync(processingDir))
153
+ recoverStaleProcessing(memoryDir, Date.now());
102
154
  if (fs.existsSync(processingDir)) {
103
155
  throw new Error(`上一批整理尚未收尾,不能重新认领 processing: ${processingDir}`);
104
156
  }
105
- else if (fs.existsSync(pendingDir)) {
157
+ if (fs.existsSync(pendingDir)) {
106
158
  try {
107
159
  const count = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md')).length;
108
160
  fs.renameSync(pendingDir, processingDir);
109
161
  claimedProcessing = true;
162
+ processingClaimedAt = Date.now();
110
163
  // 立即重建空 pending 目录,供主会话后续写 note
111
164
  fs.mkdirSync(pendingDir, { recursive: true });
165
+ writeProcessingLease(memoryDir, { version: 1, claimedAt: processingClaimedAt });
112
166
  logger.info(`[Memory] pending → processing 快照完成(${count} 条 note): ${processingDir}`);
113
167
  }
114
168
  catch (e) {
115
- // 迁移失败不阻断整理会话启动,整理会话会发现 processing 目录空并输出 NOOP
116
- logger.warn(`[Memory] pending → processing 迁移失败: ${e?.message}`);
169
+ if (claimedProcessing) {
170
+ try {
171
+ restorePendingNotes(memoryDir);
172
+ }
173
+ catch (restoreError) {
174
+ logger.error(`[Memory] pending → processing 失败后归还 note 失败: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`);
175
+ }
176
+ }
177
+ throw new Error(`pending → processing 迁移失败: ${e?.message ?? String(e)}`, { cause: e });
117
178
  }
118
179
  }
119
180
  }
@@ -189,6 +250,7 @@ export async function launchConsolidation(channel, params) {
189
250
  ...(effectiveCtx.peerKey ? { peerKey: effectiveCtx.peerKey } : {}),
190
251
  ...(feedbackOrigin ? { feedbackOrigin } : {}),
191
252
  consolidationTriggeredBy: triggeredBy,
253
+ ...(params.relationSweepId ? { relationSweepId: params.relationSweepId } : {}),
192
254
  },
193
255
  // cleanupPolicy 用默认 on_next_launch:清 evolcore session 记录;日志目录由 cleanupPreviousConsolidationLogs() 负责
194
256
  });
@@ -255,12 +317,60 @@ function restorePendingNotes(memoryDir) {
255
317
  const processingDir = path.join(memoryDir, 'processing');
256
318
  if (!fs.existsSync(processingDir))
257
319
  return;
320
+ if (!fs.lstatSync(processingDir).isDirectory())
321
+ throw new Error(`processing 不是普通目录: ${processingDir}`);
258
322
  const pendingDir = path.join(memoryDir, 'pending');
259
323
  fs.mkdirSync(pendingDir, { recursive: true });
260
324
  const stamp = Date.now().toString(36);
261
- for (const entry of fs.readdirSync(processingDir))
325
+ for (const entry of fs.readdirSync(processingDir)) {
326
+ if (entry === PROCESSING_LEASE_FILENAME) {
327
+ fs.rmSync(path.join(processingDir, entry), { force: true });
328
+ continue;
329
+ }
262
330
  moveIntoDir(path.join(processingDir, entry), pendingDir, stamp);
331
+ }
263
332
  fs.rmdirSync(processingDir);
333
+ removeProcessingLease(memoryDir);
334
+ }
335
+ function recoverStaleProcessing(memoryDir, now) {
336
+ const processingDir = path.join(memoryDir, 'processing');
337
+ const stat = fs.lstatSync(processingDir);
338
+ if (!stat.isDirectory())
339
+ throw new Error(`processing 不是普通目录: ${processingDir}`);
340
+ const lease = readProcessingLease(memoryDir);
341
+ const claimedAt = lease?.claimedAt ?? stat.mtimeMs;
342
+ const ageMs = Math.max(0, now - claimedAt);
343
+ if (ageMs < PROCESSING_LEASE_TIMEOUT_MS)
344
+ return false;
345
+ restorePendingNotes(memoryDir);
346
+ logger.warn(`[Memory] 回收过期 processing 锁并归还 note(ageMs=${ageMs}): ${processingDir}`);
347
+ return true;
348
+ }
349
+ export function recoverStaleConsolidationProcessing(selfAid, now = Date.now()) {
350
+ const root = resolveRoot();
351
+ const memoryDirs = [path.join(root, 'agents', selfAid, 'personal', 'memory')];
352
+ const relationsDir = path.join(root, 'agents', selfAid, 'relations');
353
+ try {
354
+ for (const entry of fs.readdirSync(relationsDir, { withFileTypes: true })) {
355
+ if (entry.isDirectory())
356
+ memoryDirs.push(path.join(relationsDir, entry.name, 'memory'));
357
+ }
358
+ }
359
+ catch { /* The Agent may not have relation data yet. */ }
360
+ const recovered = [];
361
+ for (const memoryDir of memoryDirs) {
362
+ const processingDir = path.join(memoryDir, 'processing');
363
+ if (!fs.existsSync(processingDir))
364
+ continue;
365
+ try {
366
+ if (recoverStaleProcessing(memoryDir, now))
367
+ recovered.push(processingDir);
368
+ }
369
+ catch (error) {
370
+ logger.warn(`[Memory] 检查 processing 租约失败: ${error instanceof Error ? error.message : String(error)}`);
371
+ }
372
+ }
373
+ return recovered;
264
374
  }
265
375
  /**
266
376
  * 整理会话结束后的收尾,task:completed / task:error / task:interrupted 都走这里。
@@ -283,6 +393,10 @@ export async function onConsolidationComplete(params) {
283
393
  fs.mkdirSync(pendingDir, { recursive: true });
284
394
  for (const entry of fs.readdirSync(processingDir)) {
285
395
  const src = path.join(processingDir, entry);
396
+ if (entry === PROCESSING_LEASE_FILENAME) {
397
+ fs.rmSync(src, { force: true });
398
+ continue;
399
+ }
286
400
  const isNote = entry.endsWith('.md') && fs.statSync(src).isFile();
287
401
  moveIntoDir(src, isNote ? noteDest : pendingDir, stamp);
288
402
  if (isNote)
@@ -291,6 +405,7 @@ export async function onConsolidationComplete(params) {
291
405
  fs.rmdirSync(processingDir);
292
406
  result.movedTo = noteDest;
293
407
  }
408
+ removeProcessingLease(params.memoryDir);
294
409
  }
295
410
  // C3: update mode-specific checkpoint only when thread snapshots were used
296
411
  if (params.success && params.chatDir && params.mode && params.usedThreadSnapshots) {
@@ -382,24 +497,52 @@ export async function rollbackConsolidation(batchDir, selfAid) {
382
497
  return { rolledBack, errors };
383
498
  }
384
499
  const MAX_CONCURRENT_CONSOLIDATION = 5;
385
- export function createConsolidationSessionFinalizer({ sessionManager, getAgent, agentMap, releaseSlot, enqueueConsolidation, sendFeedback, }) {
500
+ /** 从 ops 记录涉及的 memory 目录里挑出关系目录,返回它们的 peerKey(排除 exclude)。 */
501
+ function relationPeerKeysOf(scopes, exclude) {
502
+ const out = new Set();
503
+ for (const [scope, memoryDir] of scopes) {
504
+ if (scope !== 'relation')
505
+ continue;
506
+ // .../relations/<peerKey>/memory
507
+ const peerKey = path.basename(path.dirname(memoryDir));
508
+ if (peerKey && peerKey !== exclude)
509
+ out.add(peerKey);
510
+ }
511
+ return [...out];
512
+ }
513
+ export function createConsolidationSessionFinalizer({ sessionManager, getAgent, agentMap, releaseSlot, enqueueConsolidation, sendFeedback, onSessionFinalized, }) {
386
514
  const finalizedSessions = new Set();
387
- return async (sessionId, success) => {
515
+ return async (sessionId, success, models = {}) => {
388
516
  let releaseAid;
517
+ let finalizedInfo;
389
518
  try {
390
519
  if (!sessionId)
391
520
  return;
521
+ if (finalizedSessions.has(sessionId))
522
+ return;
523
+ // 读不到会话或批次目录时也要通知一次(只带 sessionId,链按它认领),否则链只能干等看门狗
524
+ finalizedInfo = { sessionId, success, touchedPeerKeys: [] };
392
525
  const session = await sessionManager.getSessionById(sessionId);
393
526
  if (!session)
394
527
  return;
395
528
  const meta = (session.metadata ?? {});
529
+ if (typeof meta.relationSweepId === 'string')
530
+ finalizedInfo.relationSweepId = meta.relationSweepId;
396
531
  const batchDir = (meta.consolidationBatchDir ?? meta.consolidationLogBase);
397
532
  if (!batchDir)
398
533
  return;
399
- if (finalizedSessions.has(sessionId))
400
- return;
401
534
  finalizedSessions.add(sessionId);
402
535
  releaseAid = session.selfAID;
536
+ // 先占位:后续步骤即使抛错,链也要能推进,不能卡在这一步
537
+ finalizedInfo = {
538
+ sessionId,
539
+ selfAid: session.selfAID,
540
+ peerKey: typeof meta.peerKey === 'string' ? meta.peerKey : undefined,
541
+ mode: typeof meta.memoryMode === 'string' ? meta.memoryMode : undefined,
542
+ success,
543
+ relationSweepId: typeof meta.relationSweepId === 'string' ? meta.relationSweepId : undefined,
544
+ touchedPeerKeys: [],
545
+ };
403
546
  const { summarizeOpsRecords, collectAffectedScopes } = await import('./consolidation-ops-records.js');
404
547
  // 会话正常结束不等于整理做完了。notes 模式认领了 note,却既没写记忆也没写反馈,
405
548
  // 说明 agent 没处理这批 note(例如拒绝了任务)——按失败收尾,note 回 pending 下次重试。
@@ -427,7 +570,10 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
427
570
  });
428
571
  try {
429
572
  const affectedScopes = collectAffectedScopes(batchDir);
430
- if (affectedScopes.size > 0) {
573
+ // 回滚后仍在流水里的写入不算数:失败的会话不让别的关系去整理
574
+ if (success)
575
+ finalizedInfo.touchedPeerKeys = relationPeerKeysOf(affectedScopes, finalizedInfo.peerKey);
576
+ if (affectedScopes.length > 0) {
431
577
  const { rebuildIndex } = await import('./index-manager.js');
432
578
  const selfAid = session.selfAID;
433
579
  const memoryConfig = selfAid ? getAgent(selfAid)?.config.memory : undefined;
@@ -443,12 +589,17 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
443
589
  }
444
590
  }
445
591
  catch { }
592
+ // feedback 只读一次:先落 batch.md,再清 scratch/,投递复用同一份文本。
593
+ // 同时挂到 session-end 记录上——scratch/ 收尾即删,batch.md 那段又嵌在
594
+ // frontmatter 后不好按时间线翻,ops 里留一份才能 grep 到。
595
+ const customText = readFeedbackText(batchDir);
446
596
  appendOpsRecord(batchDir, {
447
597
  ts: new Date().toISOString(),
448
598
  op: 'session-end',
449
599
  success,
450
600
  movedNotes: completeResult.moved,
451
601
  checkpointWritten: completeResult.checkpointWritten,
602
+ ...(customText ? { feedbackText: customText } : {}),
452
603
  });
453
604
  if (success) {
454
605
  const memoryMode = meta.memoryMode;
@@ -466,6 +617,9 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
466
617
  const now = Date.now();
467
618
  const affectedScopes = collectAffectedScopes(batchDir);
468
619
  for (const [scope, memoryDir] of affectedScopes) {
620
+ // 关系层的到期审查是关系整理链的最后一步(relation-sweep.ts),这里不重复拉会话
621
+ if (scope === 'relation')
622
+ continue;
469
623
  const consolidatedDir = path.join(memoryDir, 'consolidated');
470
624
  let due = 0;
471
625
  try {
@@ -519,8 +673,6 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
519
673
  fs.writeFileSync(historyPath, JSON.stringify({ note: `runner ${baseagent} does not support getSessionMessages` }) + '\n', 'utf-8');
520
674
  }
521
675
  }
522
- // feedback 只读一次:先落 batch.md,再清 scratch/,投递复用同一份文本
523
- const customText = readFeedbackText(batchDir);
524
676
  // batch.md 收尾更新:写入最终状态和 feedback 正文
525
677
  try {
526
678
  updateBatchMeta(batchDir, {
@@ -528,6 +680,8 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
528
680
  finishedAt: new Date().toISOString(),
529
681
  success,
530
682
  ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
683
+ ...(models.requestedModel ? { requestedModel: models.requestedModel } : {}),
684
+ ...(models.actualModel ? { actualModel: models.actualModel } : {}),
531
685
  ...(failureReason ? { failureReason } : {}),
532
686
  feedbackText: customText,
533
687
  });
@@ -540,7 +694,7 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
540
694
  catch { /* 静默,不影响投递 */ }
541
695
  const feedbackOrigin = meta.feedbackOrigin;
542
696
  const triggeredBy = meta.consolidationTriggeredBy;
543
- if (feedbackOrigin && triggeredBy && triggeredBy !== 'periodic-trigger' && session.selfAID) {
697
+ if (feedbackOrigin && triggeredBy && !isSilentConsolidationTrigger(triggeredBy) && session.selfAID) {
544
698
  try {
545
699
  const text = customText ?? renderDefaultConsolidationFeedback({
546
700
  mode: meta.memoryMode,
@@ -550,7 +704,18 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
550
704
  batchDir,
551
705
  failureReason,
552
706
  });
553
- await sendFeedback(feedbackOrigin, text, session.selfAID);
707
+ // 反馈是系统发起的一次性发送,不经过 runner 回复路径,context.debug 不会被自动填充。
708
+ // 这里显式补上,线上 payload 才能通过 debug.agentSessionId 回溯到本次整理会话。
709
+ // model 与 runner 回复路径同义(EvolCore 请求的模型),actualModel 为 runner 实际调用的模型。
710
+ await sendFeedback({
711
+ ...feedbackOrigin,
712
+ debug: {
713
+ sessionId: session.id,
714
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
715
+ ...(models.requestedModel ? { model: models.requestedModel } : {}),
716
+ ...(models.actualModel ? { actualModel: models.actualModel } : {}),
717
+ },
718
+ }, text, session.selfAID);
554
719
  }
555
720
  catch (error) {
556
721
  logger.warn(`[Memory] 整理反馈投递失败(session=${sessionId}): ${error?.message}`);
@@ -563,6 +728,15 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
563
728
  finally {
564
729
  if (releaseAid && sessionId)
565
730
  releaseSlot(releaseAid, sessionId);
731
+ // 槽位释放之后再通知:链的下一步入队时,本 agent 的串行队列已经空出来
732
+ if (finalizedInfo && onSessionFinalized) {
733
+ try {
734
+ onSessionFinalized(finalizedInfo);
735
+ }
736
+ catch (error) {
737
+ logger.warn(`[Memory] 整理收尾回调失败(session=${sessionId}): ${error?.message}`);
738
+ }
739
+ }
566
740
  // runtime 目录清理:immediate 策略下,finalizer 是最后一个要读它的消费者。
567
741
  // 用 sessionId 无状态重算路径,不依赖 spec 传递——保证即使 spec 不传 runtimeDir
568
742
  // 字段,目录也能在会话结束后被清掉。
@@ -602,6 +776,10 @@ export function createConsolidationFeedbackSender(getChannel) {
602
776
  replyContext.threadId = threadId;
603
777
  if (chatType)
604
778
  replyContext.delivery = { chatType };
779
+ // 诊断字典(sessionId/agentSessionId),由整理收尾流程写入 origin;缺省时不上线。
780
+ const debug = origin.debug;
781
+ if (debug)
782
+ replyContext.debug = debug;
605
783
  // 对比期:同一份反馈 notice、text 各发一条,观察两种模式在对端的差异,
606
784
  // 跑通后删掉 text 那条。两条 taskId 后缀不同,避免共用 operationId 被出站去重吞掉一条;
607
785
  // 各自 try,一条失败不连累另一条。
@@ -646,6 +824,13 @@ export function createConsolidationFeedbackSender(getChannel) {
646
824
  throw new Error(failures.join('; '));
647
825
  };
648
826
  }
827
+ /** 从 bg-session 终态事件里取出模型字段,原样交给收尾流程。 */
828
+ function modelsFromEvent(event) {
829
+ return {
830
+ ...(event.requestedModel ? { requestedModel: event.requestedModel } : {}),
831
+ ...(event.actualModel ? { actualModel: event.actualModel } : {}),
832
+ };
833
+ }
649
834
  export function registerConsolidationSessionEvents({ eventBus, permissionGateway, finalize, }) {
650
835
  eventBus.subscribe('task:interrupted', event => {
651
836
  if ('sessionId' in event && event.sessionId) {
@@ -664,15 +849,15 @@ export function registerConsolidationSessionEvents({ eventBus, permissionGateway
664
849
  });
665
850
  eventBus.subscribe('bg-session:completed', event => {
666
851
  if (event.type === 'bg-session:completed')
667
- void finalize(event.sessionId, true);
852
+ void finalize(event.sessionId, true, modelsFromEvent(event));
668
853
  });
669
854
  eventBus.subscribe('bg-session:error', event => {
670
855
  if (event.type === 'bg-session:error')
671
- void finalize(event.sessionId, false);
856
+ void finalize(event.sessionId, false, modelsFromEvent(event));
672
857
  });
673
858
  eventBus.subscribe('bg-session:interrupted', event => {
674
859
  if (event.type === 'bg-session:interrupted')
675
- void finalize(event.sessionId, false);
860
+ void finalize(event.sessionId, false, modelsFromEvent(event));
676
861
  });
677
862
  }
678
863
  export class ConsolidationQueue {
@@ -682,12 +867,20 @@ export class ConsolidationQueue {
682
867
  activeSessions = new Map();
683
868
  timers = new Map();
684
869
  finalizer;
870
+ launchFailedListener;
685
871
  constructor(daemonChannel) {
686
872
  this.daemonChannel = daemonChannel;
687
873
  }
688
874
  setFinalizer(finalizer) {
689
875
  this.finalizer = finalizer;
690
876
  }
877
+ /**
878
+ * 排队的请求轮到时拉起失败的通知。直接 enqueue 立即拉起失败时调用方已拿到 null,
879
+ * 不再重复通知;只有入队后由 drain 拉起的失败,调用方无从得知,靠这里补上。
880
+ */
881
+ setLaunchFailedListener(listener) {
882
+ this.launchFailedListener = listener;
883
+ }
691
884
  async enqueue(params) {
692
885
  const aid = params.taskCtx.selfAid;
693
886
  if (!aid)
@@ -729,7 +922,7 @@ export class ConsolidationQueue {
729
922
  this.activeAgents.delete(aid);
730
923
  void this.drain();
731
924
  }
732
- async start(params) {
925
+ async start(params, fromDrain = false) {
733
926
  const aid = params.taskCtx.selfAid;
734
927
  if (!aid)
735
928
  return null;
@@ -763,6 +956,12 @@ export class ConsolidationQueue {
763
956
  catch (error) {
764
957
  logger.warn(`[Memory] 拉起整理会话失败(aid=${aid},mode=${params.mode}): ${error?.message}`);
765
958
  this.activeAgents.delete(aid);
959
+ if (fromDrain && this.launchFailedListener) {
960
+ try {
961
+ this.launchFailedListener(params);
962
+ }
963
+ catch { /* 通知失败不影响队列 */ }
964
+ }
766
965
  void this.drain();
767
966
  return null;
768
967
  }
@@ -782,7 +981,7 @@ export class ConsolidationQueue {
782
981
  break;
783
982
  if (this.queues.get(picked.aid)?.length === 0)
784
983
  this.queues.delete(picked.aid);
785
- await this.start(picked.params);
984
+ await this.start(picked.params, true);
786
985
  }
787
986
  }
788
987
  }
@@ -803,7 +1002,7 @@ export function registerConsolidationLaunchExecutor({ ipcServer, enqueueConsolid
803
1002
  return result;
804
1003
  });
805
1004
  }
806
- export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, enqueueConsolidation, }) {
1005
+ export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, enqueueConsolidation, requestRelationSweep, }) {
807
1006
  ipcServer.setConsolidationCheckExecutor(async ({ selfAid }) => {
808
1007
  const agent = agentRegistry.get(selfAid);
809
1008
  const agentConfig = agent?.config;
@@ -811,6 +1010,7 @@ export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, e
811
1010
  return { launched: [] };
812
1011
  if (consolidationBlockedReason(selfAid))
813
1012
  return { launched: [] };
1013
+ recoverStaleConsolidationProcessing(selfAid);
814
1014
  await ownerResolver.refresh(selfAid);
815
1015
  const ownerAid = getPrimaryOwner(selfAid);
816
1016
  const agentCount = agentRegistry.runnableAgents().length;
@@ -833,6 +1033,11 @@ export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, e
833
1033
  });
834
1034
  const launched = [];
835
1035
  for (const candidate of candidates) {
1036
+ if (candidate.peerKey && requestRelationSweep) {
1037
+ requestRelationSweep(selfAid, candidate.peerKey);
1038
+ launched.push({ mode: `sweep:${candidate.mode}`, peerKey: candidate.peerKey });
1039
+ continue;
1040
+ }
836
1041
  const taskCtx = {
837
1042
  selfAid,
838
1043
  peerKey: candidate.peerKey,
@@ -928,6 +1133,7 @@ export function renderDefaultConsolidationFeedback(input) {
928
1133
  compress: '上下文压缩',
929
1134
  'session-new': '新会话',
930
1135
  'periodic-trigger': '定时检查',
1136
+ 'relation-idle': '关系空闲整理',
931
1137
  };
932
1138
  const mode = input.mode ?? 'notes';
933
1139
  const cause = triggerLabel[input.triggeredBy] ?? input.triggeredBy;