evolcore 0.0.33 → 0.0.35-test.0

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 (152) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +38 -4
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +93 -14
  6. package/dist/agents/codex/diagnostics.js +79 -22
  7. package/dist/agents/codex-runner.js +245 -47
  8. package/dist/agents/ecagent-runner.js +90 -27
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/agentmd.js +13 -2
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/identity.js +1 -1
  13. package/dist/aun/msg/managed-operation.js +16 -0
  14. package/dist/aun/msg/p2p.js +18 -2
  15. package/dist/channels/aun.js +123 -7
  16. package/dist/channels/daemon.js +6 -3
  17. package/dist/cli/aun-commands.js +39 -14
  18. package/dist/cli/ctl-command.js +3 -1
  19. package/dist/cli/daemon-commands.js +130 -23
  20. package/dist/cli/fs-command.js +2 -1
  21. package/dist/cli/getmeta-command.js +211 -0
  22. package/dist/cli/index.js +26 -6
  23. package/dist/cli/integrity-command.js +196 -0
  24. package/dist/cli/managed-command-guard.js +3 -0
  25. package/dist/cli/memory-command.js +200 -156
  26. package/dist/cli/thread-command.js +17 -7
  27. package/dist/cli/trigger-command.js +6 -1
  28. package/dist/config/config-field-policy.js +4 -0
  29. package/dist/config/config-manager.js +19 -0
  30. package/dist/config/contact-book-store.js +5 -0
  31. package/dist/config/resolved-config-op.js +25 -8
  32. package/dist/config/role-service.js +13 -0
  33. package/dist/config/runtime-env.js +16 -0
  34. package/dist/config-store.js +30 -0
  35. package/dist/core/audit/log-integrity.js +3 -1
  36. package/dist/core/auth/agent-delegation.js +108 -35
  37. package/dist/core/auth/delegation-diagnostics.js +414 -0
  38. package/dist/core/auth/operation-authorizer.js +22 -3
  39. package/dist/core/auth/operation-catalog.js +16 -0
  40. package/dist/core/channel-runtime.js +1 -1
  41. package/dist/core/command/agent-control.js +18 -26
  42. package/dist/core/command/command-handler.js +145 -5
  43. package/dist/core/command/daemon-diagnostics.js +1 -0
  44. package/dist/core/command/menu-catalog.js +3 -0
  45. package/dist/core/command/menu-handler.js +24 -7
  46. package/dist/core/command/menu-protocol.js +1 -0
  47. package/dist/core/command/slash-handler.js +2 -0
  48. package/dist/core/command/topic-slash.js +4 -7
  49. package/dist/core/daemon-bootstrap.js +4 -7
  50. package/dist/core/event-catalog.js +14 -0
  51. package/dist/core/file-integrity/read.js +61 -0
  52. package/dist/core/file-integrity/store.js +262 -0
  53. package/dist/core/ipc-registration.js +47 -2
  54. package/dist/core/memory/batch-meta.js +133 -0
  55. package/dist/core/memory/consolidation-context.js +132 -28
  56. package/dist/core/memory/consolidation-ops-records.js +14 -16
  57. package/dist/core/memory/consolidation-trigger.js +15 -7
  58. package/dist/core/memory/consolidation.js +146 -26
  59. package/dist/core/memory/index-manager.js +19 -28
  60. package/dist/core/memory/memory-admin.js +19 -14
  61. package/dist/core/memory/memory-document.js +53 -9
  62. package/dist/core/memory/memory-writer.js +50 -19
  63. package/dist/core/message/aun-ipc-sender.js +19 -1
  64. package/dist/core/message/im-renderer.js +20 -4
  65. package/dist/core/message/message-bridge.js +5 -4
  66. package/dist/core/message/message-log.js +1 -0
  67. package/dist/core/message/peer-mode.js +17 -0
  68. package/dist/core/message/response-engine.js +598 -50
  69. package/dist/core/permission/ec-command-parser.js +169 -44
  70. package/dist/core/permission/protected-paths.js +5 -1
  71. package/dist/core/permission/readonly-shell-query.js +43 -5
  72. package/dist/core/permission/session-path-grants.js +334 -0
  73. package/dist/core/permission/tool-canonical.js +226 -0
  74. package/dist/core/permission/tool-policy.js +60 -62
  75. package/dist/core/relation/peer-identity.js +87 -12
  76. package/dist/core/role/runtime-policy.js +21 -10
  77. package/dist/core/session/session-fs-store.js +123 -8
  78. package/dist/core/session/session-manager.js +131 -6
  79. package/dist/core/session/session-title.js +26 -1
  80. package/dist/core/skill/index-generator.js +17 -3
  81. package/dist/core/system-session/grants.js +40 -0
  82. package/dist/core/system-session/memory.js +289 -0
  83. package/dist/core/system-session/registry.js +74 -0
  84. package/dist/core/system-session/types.js +14 -0
  85. package/dist/core/thread/command-parser.js +6 -1
  86. package/dist/core/thread/ipc-operation.js +33 -16
  87. package/dist/core/thread/thread-operation.js +62 -24
  88. package/dist/core/thread/thread-service.js +258 -51
  89. package/dist/eck/kit-renderer.js +7 -2
  90. package/dist/eck/manifest-engine.js +24 -4
  91. package/dist/index.js +11 -2
  92. package/dist/ipc.js +51 -40
  93. package/dist/paths.js +70 -0
  94. package/dist/stats/billing.js +20 -8
  95. package/dist/trigger/feedback.js +6 -1
  96. package/dist/trigger/parser.js +8 -1
  97. package/dist/trigger/system-definitions.js +3 -3
  98. package/dist/utils/cross-platform.js +17 -5
  99. package/dist/utils/error-dict.json +18 -4
  100. package/dist/utils/error-utils.js +121 -0
  101. package/dist/utils/instance-registry.js +13 -8
  102. package/dist/utils/log-writer.js +12 -0
  103. package/dist/utils/process-introspect.js +11 -0
  104. package/kits/docs/context-assembly.md +0 -1
  105. package/kits/docs/evolcore/agent-create.md +5 -4
  106. package/kits/docs/evolcore/agent.md +2 -2
  107. package/kits/docs/evolcore/config.md +2 -2
  108. package/kits/docs/evolcore/ctl.md +2 -2
  109. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  110. package/kits/docs/evolcore/fs-architecture.md +9 -10
  111. package/kits/docs/evolcore/fs.md +2 -2
  112. package/kits/docs/evolcore/group-collaboration.md +3 -3
  113. package/kits/docs/evolcore/group.md +14 -12
  114. package/kits/docs/evolcore/memory-architecture.md +4 -5
  115. package/kits/docs/evolcore/memory-consolidation.md +19 -16
  116. package/kits/docs/evolcore/memory-tags.md +3 -1
  117. package/kits/docs/evolcore/memory.md +24 -12
  118. package/kits/docs/evolcore/model.md +16 -0
  119. package/kits/docs/evolcore/msg.md +7 -7
  120. package/kits/docs/evolcore/stats.md +1 -1
  121. package/kits/docs/evolcore/thread-chain.md +30 -14
  122. package/kits/docs/prompt-loading-architecture.md +0 -1
  123. package/kits/eck_manifest.auxiliary.json +9 -0
  124. package/kits/eck_manifest.json +20 -20
  125. package/kits/eck_manifest.memory.json +20 -12
  126. package/kits/rules/01-overview.md +11 -0
  127. package/kits/rules/03-identity.md +6 -3
  128. package/kits/rules/06-channel.md +1 -1
  129. package/kits/rules/07-memory-and-thread.md +13 -16
  130. package/kits/schemas/_meta.json +7 -1
  131. package/kits/schemas/agent-config.schema.13.json +1 -1
  132. package/kits/schemas/daemon.schema.7.json +140 -0
  133. package/kits/skills/ec-group/SKILL.md +8 -5
  134. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  135. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  136. package/kits/skills/thread-ops/SKILL.md +18 -18
  137. package/kits/templates/memory-consolidation/forget.md +2 -2
  138. package/kits/templates/memory-consolidation/generalize.md +4 -4
  139. package/kits/templates/memory-consolidation/notes.md +12 -12
  140. package/kits/templates/memory-consolidation/profile.md +3 -3
  141. package/kits/templates/memory-consolidation/promote.md +3 -3
  142. package/kits/templates/memory-consolidation/workflow.md +42 -7
  143. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  144. package/kits/templates/system-fragments/bash-description.md +9 -0
  145. package/kits/templates/system-fragments/bootstrap.md +4 -3
  146. package/kits/templates/system-fragments/channel.md +2 -2
  147. package/kits/templates/system-fragments/memory.md +16 -9
  148. package/kits/templates/system-fragments/reply.md +6 -6
  149. package/kits/templates/system-fragments/session.md +1 -1
  150. package/package.json +4 -3
  151. package/dist/core/memory/memory-session-policy.js +0 -41
  152. package/dist/core/memory/memory-signature.js +0 -257
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * consolidation-context.ts — 整理上下文 MD 文档序列化(v3)
3
3
  *
4
- * 把一次整理所需的全部信息序列化成 Markdown,写两处:
5
- * - sessionRuntimeDir(会话内读取用,随会话销毁)
6
- * - $EVOLCORE_HOME/memory/consolidation-batches/<批次目录>/(长期留存,供调试)
4
+ * 把一次整理所需的全部信息序列化成 Markdown,写一处:
5
+ * - $EVOLCORE_HOME/memory/consolidation-batches/<批次目录>/context.md
6
+ *
7
+ * 任务书只有一个落点。此前另写一份到会话运行目录(sessionRuntimeDir,缺省回落
8
+ * $EVOLCORE_HOME/tmp),导致落点随触发路径漂移,且该目录的读取范围不由任务书所在
9
+ * 的整理会话决定——整理会话读自己的任务书会被只读判定拦成 workspace_escape。
10
+ * 见 docs/memory/session-profile-design.md §5.1。
7
11
  *
8
12
  * 批次目录命名:<YYYYMMDDTHHmmss>-<selfAid>-<mode>/
9
13
  * 批次目录内:context.md(任务书)、ops.jsonl(操作流水)、threads/(话题快照)
@@ -15,6 +19,8 @@ import { resolveRoot, resolvePaths } from '../../paths.js';
15
19
  import { DEFAULT_FORGET_AFTER_DAYS, collectCandidates, isDueForReview } from './staleness.js';
16
20
  import { chatDirPath } from '../session/session-fs-store.js';
17
21
  import { getMemoryArray, getMemoryString, parseMemoryDocument } from './memory-document.js';
22
+ import { writeBatchMeta } from './batch-meta.js';
23
+ import { feedbackFilePath } from './consolidation-ops-records.js';
18
24
  export const CONSOLIDATION_MODES = ['notes', 'generalize', 'promote', 'profile', 'forget'];
19
25
  /**
20
26
  * 模式 → 允许写入的作用域(不含 forget)。
@@ -220,13 +226,50 @@ function fillCandidates(ctx, mode, forgetAfterDays) {
220
226
  else if (mode === 'forget') {
221
227
  const now = Date.now();
222
228
  const thresholds = { forgetAfterDays };
223
- ctx.candidates = collectCandidates(source, thresholds, now)
224
- .filter(info => isDueForReview(info, thresholds, now))
225
- .map(info => ({
226
- relPath: `consolidated/${info.name}`,
227
- description: readFmValue(info.content, 'description') ?? info.name.replace(/\.md$/, ''),
228
- tags: info.tags,
229
- }));
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
+ }
230
273
  }
231
274
  }
232
275
  // ── 渲染 ────────────────────────────────────────────────────────────────────
@@ -256,15 +299,45 @@ function renderScopeSection(ctx, mode, threadFiles) {
256
299
  lines.push(`待整理的 note 在 \`${ctx.processingDir}\`(整理开始前已从 pending/ 整体移入,是本次的快照)。`);
257
300
  }
258
301
  else if (ctx.candidates) {
259
- lines.push(`**候选记忆(${ctx.candidates.length} 条)** —— 只列路径与描述,按需读正文:`);
260
- lines.push('');
261
- if (ctx.candidates.length === 0) {
262
- lines.push('(无候选)');
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
+ }
263
328
  }
264
329
  else {
265
- for (const c of ctx.candidates) {
266
- const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
267
- lines.push(`- \`${c.relPath}\`${tags} — ${c.description}`);
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
+ }
268
341
  }
269
342
  }
270
343
  }
@@ -421,8 +494,6 @@ export function serializeConsolidationContext(input) {
421
494
  migrateLegacyConsolidationBatches();
422
495
  const scopes = resolveScopeContexts(input);
423
496
  const now = new Date();
424
- // 会话内文件名用:去掉所有分隔符,无 T,14 字符(YYYYMMDDHHmmss)
425
- const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
426
497
  const forgetAfterDays = input.forgetAfterDays ?? DEFAULT_FORGET_AFTER_DAYS;
427
498
  for (const ctx of scopes) {
428
499
  fillCandidates(ctx, input.mode, forgetAfterDays);
@@ -435,6 +506,9 @@ export function serializeConsolidationContext(input) {
435
506
  fs.mkdirSync(batchRoot, { recursive: true });
436
507
  const batchDir = fs.mkdtempSync(path.join(batchRoot, `${prettyTs}-${selfAid}-${input.mode}-`));
437
508
  fs.mkdirSync(batchDir, { recursive: true });
509
+ // 反馈文件的父目录先建好:整理会话只被授权写这一个文件,无权建目录,
510
+ // 而各 runner 的补丁器 / 文件写入工具在父目录不存在时会失败。
511
+ fs.mkdirSync(path.dirname(feedbackFilePath(batchDir)), { recursive: true });
438
512
  // ── 话题快照(notes / compress / session-new 模式,有 peerId 时生成)───────
439
513
  let historyJsonlPath;
440
514
  let threadFiles;
@@ -487,6 +561,22 @@ export function serializeConsolidationContext(input) {
487
561
  '',
488
562
  ...scopes.flatMap(ctx => [renderScopeSection(ctx, input.mode, threadFiles), '', '---', '']),
489
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('', '---', '');
490
580
  if (input.mode === 'notes' && threadFiles && threadFiles.length > 0) {
491
581
  const threadsDir = path.join(batchDir, 'threads');
492
582
  lines.push('## 话题快照文件', '');
@@ -512,21 +602,35 @@ export function serializeConsolidationContext(input) {
512
602
  lines.push('', '---', '');
513
603
  }
514
604
  lines.push('请按照本次会话注入的模式手册完成整理。');
605
+ lines.push('');
606
+ lines.push('整理结束后,用文件写入工具(Write / Edit)把本次整理的整体反馈写入以下**反馈文件路径**:');
607
+ lines.push('');
608
+ lines.push(`\`${feedbackFilePath(batchDir)}\``);
609
+ lines.push('');
610
+ lines.push('这是本次会话唯一可以写的文件(文件已建好,直接写即可);写别的路径会被拒绝。');
611
+ lines.push('未写入则收尾时回退到默认统计模板(模式、触发方式、耗时、新建/更新/归档条数等)。');
515
612
  const text = lines.join('\n');
516
- // ── 写会话运行目录(任务书,会话内读取用)────────────────────────────────
517
- const tmpDir = input.sessionRuntimeDir && path.isAbsolute(input.sessionRuntimeDir)
518
- ? input.sessionRuntimeDir
519
- : path.join(resolveRoot(), 'tmp');
520
- fs.mkdirSync(tmpDir, { recursive: true });
521
- const filePath = path.join(tmpDir, `memory-consolidation-${ts}.md`);
522
- fs.writeFileSync(filePath, text, 'utf-8');
523
- // ── 写批次目录(长期留存)────────────────────────────────────────────────
613
+ // ── 写批次目录(任务书唯一落点)────────────────────────────────────────────
614
+ // 不再写会话运行目录:任务书落在 $EVOLCORE_HOME/tmp 或 %TEMP%/ec-session-<hash>/
615
+ // 会同时踩两个坑——落点随触发路径而变,且会话运行时目录的读取范围不由任务书
616
+ // 所在的会话决定(见 docs/memory/session-profile-design.md §5.1)。
524
617
  const contextFilePath = path.join(batchDir, 'context.md');
525
618
  try {
526
619
  fs.writeFileSync(contextFilePath, text, 'utf-8');
527
620
  }
528
621
  catch { /* 批次任务书写入失败不影响整理 */ }
529
- return { filePath, batchDir, contextFilePath, scopes, historyJsonlPath, threadFiles, chatDir,
622
+ // 写入初始 batch.md 元数据
623
+ writeBatchMeta(batchDir, {
624
+ selfAid: input.taskCtx.selfAid ?? 'unknown',
625
+ ...(input.taskCtx.peerKey ? { peerKey: input.taskCtx.peerKey } : {}),
626
+ ...(input.ownerAid ? { ownerAid: input.ownerAid } : {}),
627
+ ...(input.taskCtx.baseagent ? { baseagent: input.taskCtx.baseagent } : {}),
628
+ mode: input.mode,
629
+ triggeredBy: input.triggeredBy,
630
+ startedAt: now.toISOString(),
631
+ status: 'running',
632
+ });
633
+ return { filePath: contextFilePath, batchDir, contextFilePath, scopes, historyJsonlPath, threadFiles, chatDir,
530
634
  ...(threadFiles?.length ? { snapshotThroughTs: now.getTime() } : {}) };
531
635
  }
532
636
  /** 会话结束后回填本次产出,追加到任务书末尾。 */
@@ -38,7 +38,7 @@ export function summarizeOpsRecords(batchDir) {
38
38
  try {
39
39
  for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) {
40
40
  const entry = JSON.parse(line);
41
- if (entry.op === 'summary' || entry.op === 'session-start' || entry.op === 'session-end' || entry.op === 'feedback-set')
41
+ if (entry.op === 'summary' || entry.op === 'session-start' || entry.op === 'session-end')
42
42
  continue;
43
43
  if (!entry.ok) {
44
44
  summary.errors++;
@@ -56,25 +56,23 @@ export function summarizeOpsRecords(batchDir) {
56
56
  return summary;
57
57
  }
58
58
  /**
59
- * 读整理会话写入的反馈文本(`ec memory feedback`)。
60
- * 取最后一条 feedback-set;没有则返回 undefined(收尾回退默认模板)。
59
+ * 整理会话的反馈文件路径。任务书给出的路径、系统会话模块声明的唯一可写文件、
60
+ * 收尾读取与清理都以此为准,不要在别处重新拼接。
61
+ */
62
+ export function feedbackFilePath(batchDir) {
63
+ return path.join(batchDir, 'scratch', 'feedback.txt');
64
+ }
65
+ /**
66
+ * 读整理会话写入的反馈文本:<batchDir>/scratch/feedback.txt。
67
+ * 文件不存在或内容为空时返回 undefined,收尾回退默认统计模板。
61
68
  */
62
69
  export function readFeedbackText(batchDir) {
63
- const filePath = path.join(batchDir, 'ops.jsonl');
64
- if (!fs.existsSync(filePath))
65
- return undefined;
66
70
  try {
67
- const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
68
- for (let i = lines.length - 1; i >= 0; i--) {
69
- const entry = JSON.parse(lines[i]);
70
- if (entry.op === 'feedback-set') {
71
- const text = entry.text;
72
- if (typeof text === 'string' && text.trim())
73
- return text;
74
- }
75
- }
71
+ const text = fs.readFileSync(feedbackFilePath(batchDir), 'utf-8').trim();
72
+ if (text)
73
+ return text;
76
74
  }
77
- catch { /* ignore */ }
75
+ catch { /* 未写反馈属正常情况 */ }
78
76
  return undefined;
79
77
  }
80
78
  /**
@@ -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
- out.push({ mode: 'forget', peerKey, scope: 'relation', reason: `${due} 条记忆超过审查周期(未续期)` });
388
- }
387
+ if (due > 0)
388
+ hasRelationForget = true;
389
389
  }
390
- // ⑤b 超期记忆 → forget(agent 级 / owner 级)
390
+ // ⑤b agent 自身的 consolidated 超期
391
391
  const agentConsolidated = path.join(root, 'agents', selfAid, 'personal', 'memory', 'consolidated');
392
- if (config.enableAgent && checkStale(agentConsolidated, now, config.forgetAfterDays) > 0) {
393
- out.push({ mode: 'forget', scope: 'agent', reason: 'agent 级记忆超期' });
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) {
@@ -12,35 +12,36 @@ import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { agentDir, resolveRoot } from '../../paths.js';
14
14
  import { serializeConsolidationContext } from './consolidation-context.js';
15
+ import { assertSystemSessionLaunchable } from '../system-session/registry.js';
16
+ import { resolveAgentLifecycle } from '../../config/lifecycle.js';
17
+ import { ensureSessionRuntimeDir } from '../../cli/task-context.js';
15
18
  import { formatChannelKey } from '../channel-loader.js';
16
19
  import { logger } from '../../utils/logger.js';
17
- import { appendOpsRecord } from './consolidation-ops-records.js';
20
+ import { appendOpsRecord, feedbackFilePath, readFeedbackText } from './consolidation-ops-records.js';
21
+ import { updateBatchMeta } from './batch-meta.js';
18
22
  import { checkConsolidationCandidates, shouldUseMetaIndex } from './consolidation-trigger.js';
19
23
  /**
20
- * 清理当前 agent 的旧整理批次。
21
- * 启动新整理时调用,保留最新的一个会话历史用于查看和调试。
24
+ * 清理旧整理批次,全局保留最新的 10 个(不区分 aid 或 mode)。
25
+ * 启动新整理时调用;selfAid 参数保留供日志上下文用,不参与过滤。
22
26
  */
23
- function cleanupPreviousConsolidationBatches(selfAid) {
27
+ function cleanupPreviousConsolidationBatches(_selfAid) {
24
28
  try {
25
29
  const batchRoot = path.join(resolveRoot(), 'memory', 'consolidation-batches');
26
30
  if (!fs.existsSync(batchRoot))
27
31
  return;
28
- // 匹配格式:YYYYMMDDTHHMMSS-<selfAid>-*
29
- const escapedAid = selfAid.replace(/\./g, '\\.');
30
- const pattern = new RegExp(`^\\d{8}T\\d{6}-${escapedAid}-`);
31
32
  const entries = fs.readdirSync(batchRoot, { withFileTypes: true })
32
- .filter(e => e.isDirectory() && pattern.test(e.name))
33
+ .filter(e => e.isDirectory())
33
34
  .map(e => ({ name: e.name, path: path.join(batchRoot, e.name) }))
34
- .sort((a, b) => b.name.localeCompare(a.name)); // 最新的在前
35
- // 保留最新的一个,删除其他
36
- for (let i = 1; i < entries.length; i++) {
35
+ .sort((a, b) => b.name.localeCompare(a.name)); // 时间戳前缀,字典序即时间序,最新的在前
36
+ const KEEP = 10;
37
+ for (let i = KEEP; i < entries.length; i++) {
37
38
  const dir = entries[i];
38
39
  try {
39
40
  fs.rmSync(dir.path, { recursive: true, force: true });
40
- logger.debug(`[Memory] 清理前一次整理批次: ${dir.name}`);
41
+ logger.debug(`[Memory] 清理旧整理批次: ${dir.name}`);
41
42
  }
42
43
  catch (e) {
43
- logger.warn(`[Memory] 清理前一次整理批次失败 ${dir.name}: ${e?.message}`);
44
+ logger.warn(`[Memory] 清理旧整理批次失败 ${dir.name}: ${e?.message}`);
44
45
  }
45
46
  }
46
47
  }
@@ -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
  * 失败抛错,调用方负责决定是否记录诊断或回传错误。
@@ -60,6 +76,13 @@ export async function launchConsolidation(channel, params) {
60
76
  const selfAid = effectiveCtx.selfAid;
61
77
  if (!selfAid)
62
78
  throw new Error('无法确定当前 agent AID(selfAid 为空)');
79
+ // 系统会话标记:类型 / subtype 必须已注册,runner 必须受支持(Gemini 拒绝)。
80
+ // 放在认领 processing 之前,拒绝时不留副作用。
81
+ const systemSession = { type: 'memory', subtype: mode };
82
+ assertSystemSessionLaunchable(systemSession, effectiveCtx.baseagent ?? 'claude');
83
+ const blockedReason = consolidationBlockedReason(selfAid);
84
+ if (blockedReason)
85
+ throw new Error(blockedReason);
63
86
  // 清理旧的整理批次(保留最新的一个历史)
64
87
  cleanupPreviousConsolidationBatches(selfAid);
65
88
  // notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
@@ -96,7 +119,6 @@ export async function launchConsolidation(channel, params) {
96
119
  triggeredBy,
97
120
  taskCtx: effectiveCtx,
98
121
  ownerAid,
99
- sessionRuntimeDir: effectiveCtx.sessionRuntimeDir,
100
122
  ownerInvolved: params.ownerInvolved,
101
123
  forgetAfterDays: params.forgetAfterDays,
102
124
  scopeOverride: params.scopeOverride,
@@ -147,6 +169,8 @@ export async function launchConsolidation(channel, params) {
147
169
  projectPath,
148
170
  prompt,
149
171
  metadata: {
172
+ // 系统会话的唯一判据:路径 / 工具 / ec 命令由 system-session/memory.ts 决定,与角色无关
173
+ systemSession,
150
174
  sessionType: 'memory',
151
175
  memoryMode: mode,
152
176
  ...(params.scopeOverride ? { memoryScope: params.scopeOverride } : {}),
@@ -154,6 +178,7 @@ export async function launchConsolidation(channel, params) {
154
178
  consolidationBatchDir: ctxFile.batchDir,
155
179
  ...(ctxFile.chatDir ? { consolidationChatDir: ctxFile.chatDir } : {}),
156
180
  ...(claimedProcessing && memoryDir ? { consolidationMemoryDir: memoryDir } : {}),
181
+ ...(ownerAid ? { consolidationOwnerAid: ownerAid } : {}),
157
182
  consolidationUsedThreadSnapshots: !!ctxFile.threadFiles?.length,
158
183
  ...(ctxFile.snapshotThroughTs ? { consolidationSnapshotThroughTs: ctxFile.snapshotThroughTs } : {}),
159
184
  ...(effectiveCtx.peerKey ? { peerKey: effectiveCtx.peerKey } : {}),
@@ -207,6 +232,20 @@ function moveIntoDir(srcPath, destDir, stamp) {
207
232
  }
208
233
  fs.renameSync(srcPath, dest);
209
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
+ }
210
249
  function restorePendingNotes(memoryDir) {
211
250
  const processingDir = path.join(memoryDir, 'processing');
212
251
  if (!fs.existsSync(processingDir))
@@ -357,6 +396,17 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
357
396
  finalizedSessions.add(sessionId);
358
397
  releaseAid = session.selfAID;
359
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
+ }
360
410
  if (!success) {
361
411
  await rollbackConsolidation(batchDir, session.selfAID);
362
412
  }
@@ -380,9 +430,8 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
380
430
  for (const [scope, memoryDir] of affectedScopes) {
381
431
  const consolidatedDir = path.join(memoryDir, 'consolidated');
382
432
  const indexPath = path.join(memoryDir, 'MEMORY.md');
383
- const invalidDir = path.join(memoryDir, 'archive', 'invalid');
384
433
  try {
385
- await rebuildIndex(consolidatedDir, indexPath, scope, invalidDir, undefined, maxBytes);
434
+ await rebuildIndex(consolidatedDir, indexPath, scope, maxBytes);
386
435
  }
387
436
  catch { }
388
437
  }
@@ -464,18 +513,36 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
464
513
  fs.writeFileSync(historyPath, JSON.stringify({ note: `runner ${baseagent} does not support getSessionMessages` }) + '\n', 'utf-8');
465
514
  }
466
515
  }
516
+ // feedback 只读一次:先落 batch.md,再清 scratch/,投递复用同一份文本
517
+ const customText = readFeedbackText(batchDir);
518
+ // batch.md 收尾更新:写入最终状态和 feedback 正文
519
+ try {
520
+ updateBatchMeta(batchDir, {
521
+ status: success ? 'completed' : 'failed',
522
+ finishedAt: new Date().toISOString(),
523
+ success,
524
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
525
+ ...(failureReason ? { failureReason } : {}),
526
+ feedbackText: customText,
527
+ });
528
+ }
529
+ catch { /* 失败静默 */ }
530
+ // scratch/ 是会话期临时工作区,feedback 已落 batch.md,不分成败与触发方式一律清掉
531
+ try {
532
+ fs.rmSync(path.dirname(feedbackFilePath(batchDir)), { recursive: true, force: true });
533
+ }
534
+ catch { /* 静默,不影响投递 */ }
467
535
  const feedbackOrigin = meta.feedbackOrigin;
468
536
  const triggeredBy = meta.consolidationTriggeredBy;
469
537
  if (feedbackOrigin && triggeredBy && triggeredBy !== 'periodic-trigger' && session.selfAID) {
470
538
  try {
471
- const { readFeedbackText } = await import('./consolidation-ops-records.js');
472
- const customText = readFeedbackText(batchDir);
473
539
  const text = customText ?? renderDefaultConsolidationFeedback({
474
540
  mode: meta.memoryMode,
475
541
  triggeredBy,
476
542
  success,
477
543
  summary: summarizeOpsRecords(batchDir),
478
544
  batchDir,
545
+ failureReason,
479
546
  });
480
547
  await sendFeedback(feedbackOrigin, text, session.selfAID);
481
548
  }
@@ -490,6 +557,21 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
490
557
  finally {
491
558
  if (releaseAid && sessionId)
492
559
  releaseSlot(releaseAid, sessionId);
560
+ // runtime 目录清理:immediate 策略下,finalizer 是最后一个要读它的消费者。
561
+ // 用 sessionId 无状态重算路径,不依赖 spec 传递——保证即使 spec 不传 runtimeDir
562
+ // 字段,目录也能在会话结束后被清掉。
563
+ if (sessionId) {
564
+ const runtimeDir = ensureSessionRuntimeDir(sessionId);
565
+ if (runtimeDir) {
566
+ try {
567
+ fs.rmSync(runtimeDir, { recursive: true, force: true });
568
+ logger.debug(`[Memory] 清理 runtime 目录: ${runtimeDir}`);
569
+ }
570
+ catch (cleanupError) {
571
+ logger.warn(`[Memory] runtime 目录清理失败(session=${sessionId}): ${cleanupError?.message}`);
572
+ }
573
+ }
574
+ }
493
575
  }
494
576
  };
495
577
  }
@@ -514,19 +596,48 @@ export function createConsolidationFeedbackSender(getChannel) {
514
596
  replyContext.threadId = threadId;
515
597
  if (chatType)
516
598
  replyContext.delivery = { chatType };
517
- await binding.adapter.send(buildEnvelope({
518
- taskId: `memory-feedback-${Date.now()}`,
599
+ // 对比期:同一份反馈 notice、text 各发一条,观察两种模式在对端的差异,
600
+ // 跑通后删掉 text 那条。两条 taskId 后缀不同,避免共用 operationId 被出站去重吞掉一条;
601
+ // 各自 try,一条失败不连累另一条。
602
+ const stamp = Date.now();
603
+ const envelopeFor = (suffix) => buildEnvelope({
604
+ taskId: `memory-feedback-${stamp}-${suffix}`,
519
605
  channel: binding.adapter.channelKey,
520
606
  channelId,
521
607
  agentName: binding.agentName,
522
608
  replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
523
- }), {
524
- kind: 'result.text',
525
- text,
526
- isFinal: true,
527
- format: 'markdown',
528
609
  });
529
- logger.info(`[Memory] 整理反馈已投递(aid=${selfAid},channelId=${channelId})`);
610
+ const attempts = [
611
+ {
612
+ mode: 'notice',
613
+ send: () => binding.adapter.send(envelopeFor('notice'), {
614
+ kind: 'system.notice',
615
+ text,
616
+ subtype: 'memory.consolidation.result',
617
+ }),
618
+ },
619
+ {
620
+ mode: 'text',
621
+ send: () => binding.adapter.send(envelopeFor('text'), {
622
+ kind: 'result.text',
623
+ text,
624
+ isFinal: true,
625
+ format: 'markdown',
626
+ }),
627
+ },
628
+ ];
629
+ const failures = [];
630
+ for (const { mode, send } of attempts) {
631
+ try {
632
+ await send();
633
+ logger.info(`[Memory] 整理反馈已投递(mode=${mode},aid=${selfAid},channelId=${channelId})`);
634
+ }
635
+ catch (error) {
636
+ failures.push(`${mode}: ${error?.message}`);
637
+ }
638
+ }
639
+ if (failures.length > 0)
640
+ throw new Error(failures.join('; '));
530
641
  };
531
642
  }
532
643
  export function registerConsolidationSessionEvents({ eventBus, permissionGateway, finalize, }) {
@@ -673,6 +784,10 @@ export function registerConsolidationLaunchExecutor({ ipcServer, enqueueConsolid
673
784
  ipcServer.setConsolidationLaunchExecutor(async (params) => {
674
785
  if (!params.taskCtx.selfAid)
675
786
  throw new Error('无法确定当前 agent AID(selfAid 为空)');
787
+ // 入队前就拦:拉起失败在队列里只会记成一句笼统的「拉起整理会话失败」
788
+ const blockedReason = consolidationBlockedReason(params.taskCtx.selfAid);
789
+ if (blockedReason)
790
+ throw new Error(blockedReason);
676
791
  const launched = await enqueueConsolidation(params);
677
792
  if (!launched)
678
793
  throw new Error('拉起整理会话失败,详见 daemon 日志');
@@ -687,6 +802,8 @@ export function registerConsolidationCheckExecutor({ ipcServer, agentRegistry, e
687
802
  const agentConfig = agentRegistry.get(selfAid)?.config;
688
803
  if (agentConfig?.memory?.enableRelation === false)
689
804
  return { launched: [] };
805
+ if (consolidationBlockedReason(selfAid))
806
+ return { launched: [] };
690
807
  const ownerAid = agentConfig?.owners?.[0];
691
808
  const agentCount = agentRegistry.runnableAgents().length;
692
809
  const relationsDir = path.join(agentDir(selfAid), 'relations');
@@ -743,6 +860,8 @@ export function registerRelationConsolidationEvents({ eventBus, agentRegistry, r
743
860
  return;
744
861
  if (taskCtx.sessionType === 'memory')
745
862
  return;
863
+ if (consolidationBlockedReason(taskCtx.selfAid))
864
+ return;
746
865
  const memoryConfig = agentRegistry.get(taskCtx.selfAid)?.config?.memory;
747
866
  if (memoryConfig?.enableRelation === false)
748
867
  return;
@@ -807,6 +926,7 @@ export function renderDefaultConsolidationFeedback(input) {
807
926
  '',
808
927
  `**模式**:${mode}`,
809
928
  `**触发**:${cause}`,
929
+ ...(input.failureReason ? [`**原因**:${input.failureReason}`] : []),
810
930
  '',
811
931
  '本次整理未完成,已回退未归档的改动。详情见整理批次:',
812
932
  `\`${input.batchDir}\``,