evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -0,0 +1,505 @@
1
+ /**
2
+ * consolidation-context.ts — 整理上下文 MD 文档序列化(v3)
3
+ *
4
+ * 把一次整理所需的全部信息序列化成 Markdown,写两处:
5
+ * - sessionRuntimeDir(会话内读取用,随会话销毁)
6
+ * - $EVOLCORE_HOME/memory/consolidation-batches/<批次目录>/(长期留存,供调试)
7
+ *
8
+ * 批次目录命名:<YYYYMMDDTHHmmss>-<selfAid>-<mode>/
9
+ * 批次目录内:context.md(任务书)、ops.jsonl(操作流水)、threads/(话题快照)
10
+ * checkpoint(consolidation-checkpoint.json)记录上次整理时间,供过滤增量消息用。
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { resolveRoot, resolvePaths } from '../../paths.js';
15
+ import { DEFAULT_FORGET_AFTER_DAYS, collectCandidates, isDueForReview } from './staleness.js';
16
+ import { chatDirPath } from '../session/session-fs-store.js';
17
+ import { getMemoryArray, getMemoryString, parseMemoryDocument } from './memory-document.js';
18
+ export const CONSOLIDATION_MODES = ['notes', 'generalize', 'promote', 'profile', 'forget'];
19
+ /**
20
+ * 模式 → 允许写入的作用域(不含 forget)。
21
+ *
22
+ * forget 的写入作用域由**触发层级**决定(relation / agent / owner),不走这张表。
23
+ * ipc.ts 的 assertMemoryWriteSession 从 taskCtx.memoryScope 读取。
24
+ *
25
+ * 注:forget 模式同样支持「合并」产出(`create --scope <layer> --replaces ...`),
26
+ * 因此它的允许作用域是动态的,无法在此静态声明。
27
+ */
28
+ export const MODE_SCOPE = {
29
+ notes: 'relation',
30
+ generalize: 'agent',
31
+ promote: 'owner',
32
+ profile: 'relation',
33
+ };
34
+ // 超期判据集中在 staleness.ts;这里做转发,保证 trigger 与 context 用同一套规则
35
+ export { DEFAULT_FORGET_AFTER_DAYS, LONG_TERM_REVIEW_DAYS } from './staleness.js';
36
+ // ── Checkpoint 读写 ──────────────────────────────────────────────────────────
37
+ const CHECKPOINT_FILE = 'consolidation-checkpoint.json';
38
+ /**
39
+ * Move the pre-0.0.29 consolidation artifact root to its new name. Existing
40
+ * batches are user data, so conflicts are preserved under a legacy suffix
41
+ * rather than silently overwritten.
42
+ */
43
+ export function migrateLegacyConsolidationBatches() {
44
+ const root = resolveRoot();
45
+ const legacyRoot = path.join(root, 'memory', 'consolidation-logs');
46
+ const currentRoot = path.join(root, 'memory', 'consolidation-batches');
47
+ if (!fs.existsSync(legacyRoot))
48
+ return;
49
+ const moveTree = (source, target) => {
50
+ const stat = fs.lstatSync(source);
51
+ if (stat.isDirectory()) {
52
+ fs.mkdirSync(target, { recursive: true });
53
+ for (const entry of fs.readdirSync(source))
54
+ moveTree(path.join(source, entry), path.join(target, entry));
55
+ fs.rmSync(source, { recursive: true, force: true });
56
+ return;
57
+ }
58
+ fs.mkdirSync(path.dirname(target), { recursive: true });
59
+ if (!fs.existsSync(target)) {
60
+ fs.renameSync(source, target);
61
+ return;
62
+ }
63
+ const conflict = `${target}.legacy-${Date.now()}`;
64
+ fs.renameSync(source, conflict);
65
+ };
66
+ fs.mkdirSync(currentRoot, { recursive: true });
67
+ for (const entry of fs.readdirSync(legacyRoot)) {
68
+ moveTree(path.join(legacyRoot, entry), path.join(currentRoot, entry));
69
+ }
70
+ fs.rmSync(legacyRoot, { recursive: true, force: true });
71
+ }
72
+ /** 读上次整理时间戳(毫秒)。文件不存在或解析失败返回 0(取全量历史)。 */
73
+ export function readCheckpoint(chatDir) {
74
+ try {
75
+ const raw = fs.readFileSync(path.join(chatDir, CHECKPOINT_FILE), 'utf-8');
76
+ const parsed = JSON.parse(raw);
77
+ const v = Number(parsed.lastConsolidatedAt);
78
+ return Number.isFinite(v) ? v : 0;
79
+ }
80
+ catch {
81
+ return 0;
82
+ }
83
+ }
84
+ /** 写整理完成时间戳。由 index.ts 在整理会话完成后调用。 */
85
+ export function writeCheckpoint(chatDir, lastConsolidatedAt) {
86
+ try {
87
+ fs.mkdirSync(chatDir, { recursive: true });
88
+ fs.writeFileSync(path.join(chatDir, CHECKPOINT_FILE), JSON.stringify({ lastConsolidatedAt }, null, 2) + '\n', 'utf-8');
89
+ }
90
+ catch { /* 失败静默 */ }
91
+ }
92
+ // ── 作用域解析 ──────────────────────────────────────────────────────────────
93
+ function memoryDirOf(scope, selfAid, peerKey, ownerAid) {
94
+ const root = resolveRoot();
95
+ if (scope === 'agent')
96
+ return path.join(root, 'agents', selfAid, 'personal', 'memory');
97
+ if (scope === 'relation') {
98
+ if (!peerKey)
99
+ throw new Error('relation 作用域需要 peerKey');
100
+ return path.join(root, 'agents', selfAid, 'relations', peerKey, 'memory');
101
+ }
102
+ if (!ownerAid)
103
+ throw new Error('owner 作用域需要 ownerAid');
104
+ return path.join(root, 'memory', 'owner', ownerAid);
105
+ }
106
+ function buildScopeContext(scope, input, selfAid,
107
+ /** 候选来源作用域,默认与本作用域一致 */
108
+ sourceScope) {
109
+ const memoryDir = memoryDirOf(scope, selfAid, input.taskCtx.peerKey, input.ownerAid);
110
+ const candidateSourceDir = sourceScope === undefined || sourceScope === scope
111
+ ? path.join(memoryDir, 'consolidated')
112
+ : path.join(memoryDirOf(sourceScope, selfAid, input.taskCtx.peerKey, input.ownerAid), 'consolidated');
113
+ return {
114
+ scope,
115
+ selfAid,
116
+ peerKey: scope === 'relation' ? input.taskCtx.peerKey : undefined,
117
+ ownerAid: scope === 'owner' ? input.ownerAid : undefined,
118
+ agentSessionId: scope === 'relation' ? input.taskCtx.sessionId : undefined,
119
+ baseagent: input.taskCtx.baseagent,
120
+ pendingDir: path.join(memoryDir, 'pending'),
121
+ processingDir: path.join(memoryDir, 'processing'),
122
+ consolidatedDir: path.join(memoryDir, 'consolidated'),
123
+ indexPath: path.join(memoryDir, 'MEMORY.md'),
124
+ candidateSourceDir,
125
+ };
126
+ }
127
+ /**
128
+ * 按模式解析出需要渲染的作用域上下文。
129
+ * notes / profile 只看当前关系;generalize 看 agent;promote 看 owner。
130
+ */
131
+ function resolveScopeContexts(input) {
132
+ const selfAid = input.taskCtx.selfAid;
133
+ if (!selfAid)
134
+ throw new Error('selfAid 未找到,无法确定整理目标');
135
+ // forget 的可写作用域由触发层级(scopeOverride)决定,不走 MODE_SCOPE 静态表。
136
+ const scope = input.mode === 'forget'
137
+ ? input.scopeOverride
138
+ : MODE_SCOPE[input.mode];
139
+ if (!scope) {
140
+ throw new Error(`forget 模式必须提供 scopeOverride(relation / agent / owner)`);
141
+ }
142
+ if (scope === 'owner' && !input.ownerAid) {
143
+ throw new Error('promote 模式需要 ownerAid(当前 agent 未配置 owner)');
144
+ }
145
+ // generalize / promote 的输入都在关系级,产出才在 agent / owner 级。
146
+ const sourceScope = (input.mode === 'generalize' || input.mode === 'promote') ? 'relation' : undefined;
147
+ return [buildScopeContext(scope, input, selfAid, sourceScope)];
148
+ }
149
+ // ── 候选扫描 ────────────────────────────────────────────────────────────────
150
+ /** 扫 consolidated/ 目录,按判据挑出候选。 */
151
+ function readFmArray(content, key) {
152
+ return getMemoryArray(parseMemoryDocument(content), key);
153
+ }
154
+ function readFmValue(content, key) {
155
+ return getMemoryString(parseMemoryDocument(content), key);
156
+ }
157
+ function scanCandidates(consolidatedDir, pick) {
158
+ if (!fs.existsSync(consolidatedDir))
159
+ return [];
160
+ const out = [];
161
+ for (const f of fs.readdirSync(consolidatedDir)) {
162
+ if (!f.endsWith('.md'))
163
+ continue;
164
+ const fp = path.join(consolidatedDir, f);
165
+ let content;
166
+ let mtimeMs;
167
+ try {
168
+ content = fs.readFileSync(fp, 'utf-8');
169
+ mtimeMs = fs.statSync(fp).mtimeMs;
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ const tags = readFmArray(content, 'tags');
175
+ if (!pick({ tags, mtimeMs, content }))
176
+ continue;
177
+ out.push({
178
+ relPath: `consolidated/${f}`,
179
+ description: readFmValue(content, 'description') ?? f.replace(/\.md$/, ''),
180
+ tags,
181
+ });
182
+ }
183
+ out.sort((a, b) => a.relPath.localeCompare(b.relPath));
184
+ return out;
185
+ }
186
+ /** 按模式填充候选清单。 */
187
+ function fillCandidates(ctx, mode, forgetAfterDays) {
188
+ const source = ctx.candidateSourceDir ?? ctx.consolidatedDir;
189
+ if (mode === 'generalize') {
190
+ ctx.candidates = scanCandidates(source, ({ tags }) => tags.includes('to-generalize'));
191
+ }
192
+ else if (mode === 'promote') {
193
+ ctx.candidates = scanCandidates(source, ({ tags }) => tags.includes('to-promote'));
194
+ }
195
+ else if (mode === 'profile') {
196
+ ctx.candidates = scanCandidates(source, () => true);
197
+ }
198
+ else if (mode === 'forget') {
199
+ const now = Date.now();
200
+ const thresholds = { forgetAfterDays };
201
+ ctx.candidates = collectCandidates(source, thresholds, now)
202
+ .filter(info => isDueForReview(info, thresholds, now))
203
+ .map(info => ({
204
+ relPath: `consolidated/${info.name}`,
205
+ description: readFmValue(info.content, 'description') ?? info.name.replace(/\.md$/, ''),
206
+ tags: info.tags,
207
+ }));
208
+ }
209
+ }
210
+ // ── 渲染 ────────────────────────────────────────────────────────────────────
211
+ function renderScopeSection(ctx, mode, threadFiles) {
212
+ const lines = [];
213
+ lines.push(`### 作用域:${ctx.scope}`);
214
+ lines.push('');
215
+ lines.push(`- selfAid: ${ctx.selfAid}`);
216
+ if (ctx.peerKey)
217
+ lines.push(`- peerKey: ${ctx.peerKey}`);
218
+ if (ctx.ownerAid)
219
+ lines.push(`- ownerAid: ${ctx.ownerAid}`);
220
+ if (ctx.baseagent)
221
+ lines.push(`- baseagent: ${ctx.baseagent}`);
222
+ lines.push(`- processingDir: ${ctx.processingDir}`);
223
+ lines.push(`- consolidatedDir: ${ctx.consolidatedDir}`);
224
+ lines.push(`- indexPath: ${ctx.indexPath}`);
225
+ lines.push('');
226
+ if (mode === 'notes') {
227
+ if (threadFiles && threadFiles.length > 0) {
228
+ lines.push(`**对话历史**:已按话题切分为 ${threadFiles.length} 个快照文件,见下方"话题快照文件"节。`);
229
+ }
230
+ else {
231
+ lines.push('**对话历史**:无增量消息(checkpoint 之后无新消息,或定时触发无绑定会话)。');
232
+ }
233
+ lines.push('');
234
+ lines.push(`待整理的 note 在 \`${ctx.processingDir}\`(整理开始前已从 pending/ 整体移入,是本次的快照)。`);
235
+ }
236
+ else if (ctx.candidates) {
237
+ lines.push(`**候选记忆(${ctx.candidates.length} 条)** —— 只列路径与描述,按需读正文:`);
238
+ lines.push('');
239
+ if (ctx.candidates.length === 0) {
240
+ lines.push('(无候选)');
241
+ }
242
+ else {
243
+ for (const c of ctx.candidates) {
244
+ const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
245
+ lines.push(`- \`${c.relPath}\`${tags} — ${c.description}`);
246
+ }
247
+ }
248
+ }
249
+ return lines.join('\n');
250
+ }
251
+ const MODE_TITLE = {
252
+ notes: '整理 note → 关系级记忆',
253
+ generalize: '泛化:关系级 → agent 级',
254
+ promote: '提升:关系级 → owner 级',
255
+ profile: '生成对端画像',
256
+ forget: '遗忘:评估超期记忆',
257
+ };
258
+ // ── 話题快照生成 ────────────────────────────────────────────────────────────
259
+ /**
260
+ * 把 "2026-10-04 13:31:44.957" 精简为 "10-04 13:31:44":
261
+ * 去掉年份前缀(YYYY-)和毫秒(.xxx)。时间范围头部保留完整原始值供人参考。
262
+ */
263
+ function shortTime(time) {
264
+ // 匹配 "YYYY-MM-DD HH:mm:ss.mmm" 或 "YYYY-MM-DD HH:mm:ss"
265
+ const m = time.match(/^\d{4}-(\d{2}-\d{2} \d{2}:\d{2}:\d{2})(?:\.\d+)?$/);
266
+ return m ? m[1] : time;
267
+ }
268
+ /**
269
+ * 从 messages.jsonl 读取 ts >= sinceMs 的消息,按 threadId 分组,
270
+ * 每个 thread 写一个 Markdown 快照到 threadsDir/。
271
+ *
272
+ * 快照格式优化:
273
+ * - 头部标注来源文件路径、会话类型(私聊/群聊)、对端信息
274
+ * - 私聊:每行用户消息只显示名字(不重复 AID),群聊:头部建发言人清单,每行只用名字
275
+ * - 时间精简:去掉年份前缀和毫秒,节省字节
276
+ *
277
+ * 返回生成的文件路径列表(空列表表示无增量消息或读取失败)。
278
+ */
279
+ function buildThreadSnapshots(historyJsonlPath, sinceMs, threadsDir) {
280
+ const rawLines = fs.readFileSync(historyJsonlPath, 'utf-8').split('\n');
281
+ // 第一遍:扫全量行(不限 sinceMs)收集会话元信息
282
+ let chatType = 'private';
283
+ let groupId = '';
284
+ // aid → peerName 映射,用于发言人清单
285
+ const speakerNames = new Map();
286
+ for (const line of rawLines) {
287
+ if (!line.trim())
288
+ continue;
289
+ try {
290
+ const entry = JSON.parse(line);
291
+ if (typeof entry.chatType === 'string')
292
+ chatType = entry.chatType;
293
+ if (typeof entry.groupId === 'string' && entry.groupId)
294
+ groupId = entry.groupId;
295
+ // 收集入站发言人 aid → peerName
296
+ if (entry.dir === 'in') {
297
+ const from = typeof entry.from === 'string' ? entry.from : '';
298
+ const peerName = typeof entry.peerName === 'string' ? entry.peerName : '';
299
+ if (from && peerName && !speakerNames.has(from)) {
300
+ speakerNames.set(from, peerName);
301
+ }
302
+ }
303
+ }
304
+ catch { /* ignore */ }
305
+ }
306
+ const groups = new Map();
307
+ // 第二遍:只取 sinceMs 之后的消息
308
+ for (let i = 0; i < rawLines.length; i++) {
309
+ const line = rawLines[i].trim();
310
+ if (!line)
311
+ continue;
312
+ try {
313
+ const entry = JSON.parse(line);
314
+ const ts = typeof entry.ts === 'number' ? entry.ts : 0;
315
+ if (ts < sinceMs)
316
+ continue;
317
+ const content = typeof entry.content === 'string' ? entry.content.trim() : '';
318
+ if (!content)
319
+ continue;
320
+ const dir = typeof entry.dir === 'string' ? entry.dir : '';
321
+ const time = typeof entry.time === 'string' ? entry.time : '';
322
+ const from = typeof entry.from === 'string' ? entry.from : '';
323
+ const peerName = typeof entry.peerName === 'string' ? entry.peerName : '';
324
+ let speaker;
325
+ if (dir === 'in') {
326
+ // 用名字代替 AID;名字优先从当前消息取,其次从全量扫描的映射里取
327
+ const name = peerName || speakerNames.get(from) || from;
328
+ if (chatType === 'group') {
329
+ // 群聊:格式为 "[名字]",完整 AID 已在头部清单里
330
+ speaker = `[${name}]`;
331
+ }
332
+ else {
333
+ // 私聊:头部已标注对端,行内只用名字
334
+ speaker = `[${name}]`;
335
+ }
336
+ }
337
+ else {
338
+ speaker = '[助手]';
339
+ }
340
+ const threadId = typeof entry.threadId === 'string' && entry.threadId ? entry.threadId : 'main';
341
+ if (!groups.has(threadId))
342
+ groups.set(threadId, []);
343
+ groups.get(threadId).push({ lineNo: i + 1, time, speaker, content });
344
+ }
345
+ catch { /* 跳过无法解析的行 */ }
346
+ }
347
+ if (groups.size === 0)
348
+ return [];
349
+ fs.mkdirSync(threadsDir, { recursive: true });
350
+ const files = [];
351
+ // 构造头部:来源路径 + 会话类型 + 对端信息
352
+ const headerBase = [
353
+ `> 来源:\`${historyJsonlPath}\``,
354
+ ];
355
+ if (chatType === 'group') {
356
+ headerBase.push(`> 会话类型:群聊 `);
357
+ headerBase.push(`> 群 AID:${groupId} `);
358
+ if (speakerNames.size > 0) {
359
+ headerBase.push(`> 发言人清单:`);
360
+ for (const [aid, name] of speakerNames) {
361
+ headerBase.push(`> - ${name}(${aid})`);
362
+ }
363
+ }
364
+ }
365
+ else {
366
+ // 私聊:找对端 AID(入站消息的 from)
367
+ const peerAid = speakerNames.size > 0 ? [...speakerNames.keys()][0] : '';
368
+ const peerName = peerAid ? speakerNames.get(peerAid) ?? peerAid : '';
369
+ headerBase.push(`> 会话类型:私聊 `);
370
+ if (peerName)
371
+ headerBase.push(`> 对端:${peerName}(${peerAid}) `);
372
+ }
373
+ for (const [threadId, msgs] of groups) {
374
+ const firstTime = msgs[0]?.time ?? '';
375
+ const lastTime = msgs[msgs.length - 1]?.time ?? '';
376
+ const mdLines = [
377
+ `# 话题快照:${threadId}`,
378
+ '',
379
+ `> 时间范围:${firstTime} — ${lastTime} `,
380
+ ...headerBase,
381
+ '> 原始 JSONL 行号已标注,需要完整元数据时按行号读取来源文件。',
382
+ '',
383
+ ];
384
+ for (const m of msgs) {
385
+ // 时间精简:去掉年份和毫秒
386
+ mdLines.push(`L${m.lineNo} ${shortTime(m.time)} ${m.speaker}: ${m.content.replace(/\n/g, ' ')}`);
387
+ }
388
+ const safeName = threadId.replace(/[^a-zA-Z0-9._-]/g, '_');
389
+ const fp = path.join(threadsDir, safeName + '.md');
390
+ fs.writeFileSync(fp, mdLines.join('\n'), 'utf-8');
391
+ files.push(fp);
392
+ }
393
+ return files;
394
+ }
395
+ /**
396
+ * 序列化整理上下文,写会话运行目录与长期批次目录,返回两者路径。
397
+ */
398
+ export function serializeConsolidationContext(input) {
399
+ migrateLegacyConsolidationBatches();
400
+ const scopes = resolveScopeContexts(input);
401
+ const now = new Date();
402
+ // 会话内文件名用:去掉所有分隔符,无 T,14 字符(YYYYMMDDHHmmss)
403
+ const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
404
+ const forgetAfterDays = input.forgetAfterDays ?? DEFAULT_FORGET_AFTER_DAYS;
405
+ for (const ctx of scopes) {
406
+ fillCandidates(ctx, input.mode, forgetAfterDays);
407
+ }
408
+ // ── 批次目录(长期批次)────────────────────────────────────────────────────
409
+ const batchRoot = path.join(resolveRoot(), 'memory', 'consolidation-batches');
410
+ const selfAid = input.taskCtx.selfAid ?? 'unknown';
411
+ // 目录名:YYYYMMDDTHHmmss-<selfAid>-<mode>
412
+ const prettyTs = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
413
+ const batchDir = path.join(batchRoot, `${prettyTs}-${selfAid}-${input.mode}`);
414
+ fs.mkdirSync(batchDir, { recursive: true });
415
+ // ── 话题快照(notes / compress / session-new 模式,有 peerId 时生成)───────
416
+ let historyJsonlPath;
417
+ let threadFiles;
418
+ let chatDir;
419
+ if ((input.mode === 'notes' || input.triggeredBy === 'compress' || input.triggeredBy === 'session-new')
420
+ && input.taskCtx.channel
421
+ && input.taskCtx.peerId
422
+ && input.taskCtx.selfAid) {
423
+ try {
424
+ const sessionsDir = resolvePaths().sessionsDir;
425
+ chatDir = chatDirPath(sessionsDir, input.taskCtx.channel, input.taskCtx.peerId, input.taskCtx.selfAid);
426
+ const rawJsonlPath = path.join(chatDir, 'messages.jsonl');
427
+ if (fs.existsSync(rawJsonlPath)) {
428
+ historyJsonlPath = rawJsonlPath;
429
+ const sinceMs = readCheckpoint(chatDir);
430
+ const threadsDir = path.join(batchDir, 'threads');
431
+ try {
432
+ const files = buildThreadSnapshots(rawJsonlPath, sinceMs, threadsDir);
433
+ if (files.length > 0)
434
+ threadFiles = files;
435
+ }
436
+ catch { /* 快照生成失败不影响整理 */ }
437
+ }
438
+ }
439
+ catch { /* 找不到历史文件不影响整理 */ }
440
+ }
441
+ // ── 渲染任务书 ─────────────────────────────────────────────────────────────
442
+ const allowedScope = scopes[0]?.scope;
443
+ const lines = [
444
+ '# 记忆整理上下文',
445
+ '',
446
+ `> 生成时间:${now.toISOString()} `,
447
+ `> 整理模式:**${input.mode}**(${MODE_TITLE[input.mode]}) `,
448
+ `> triggeredBy: ${input.triggeredBy} `,
449
+ `> 允许写入的 scope: ${allowedScope ?? '无(本模式不产生新记忆)'} `,
450
+ `> 作用域数量: ${scopes.length}`,
451
+ '',
452
+ '---',
453
+ '',
454
+ '## 整理目标',
455
+ '',
456
+ ...scopes.flatMap(ctx => [renderScopeSection(ctx, input.mode, threadFiles), '', '---', '']),
457
+ ];
458
+ if (input.mode === 'notes' && threadFiles && threadFiles.length > 0) {
459
+ const threadsDir = path.join(batchDir, 'threads');
460
+ lines.push('## 话题快照文件', '');
461
+ lines.push(`话题快照目录:\`${threadsDir}\``, '');
462
+ lines.push('以下文件各对应一个话题,包含本次整理范围内的增量对话(checkpoint 之后):', '');
463
+ for (const f of threadFiles) {
464
+ lines.push(`- \`${path.basename(f)}\``);
465
+ }
466
+ lines.push('');
467
+ lines.push('逐一读取这些文件以了解对话上下文,辅助判断 note 的准确性和遗漏情况。');
468
+ lines.push('', '---', '');
469
+ }
470
+ if (input.mode === 'notes' && historyJsonlPath) {
471
+ lines.push('## 原始消息流水', '');
472
+ lines.push(`- 完整 JSONL(按行号随机读取元数据):\`${historyJsonlPath}\``);
473
+ lines.push('', '---', '');
474
+ }
475
+ if (input.mode === 'promote' && input.ownerInvolved !== undefined) {
476
+ lines.push('## 权限提示', '');
477
+ lines.push(input.ownerInvolved
478
+ ? '本次对话中 owner 本人参与发言,群关系下打 `to-promote` 是被允许的。'
479
+ : '本次对话中 owner 未参与发言,群关系下的记忆不应打 `to-promote`。');
480
+ lines.push('', '---', '');
481
+ }
482
+ lines.push('请按照本次会话注入的模式手册完成整理。');
483
+ const text = lines.join('\n');
484
+ // ── 写会话运行目录(任务书,会话内读取用)────────────────────────────────
485
+ const tmpDir = input.sessionRuntimeDir && path.isAbsolute(input.sessionRuntimeDir)
486
+ ? input.sessionRuntimeDir
487
+ : path.join(resolveRoot(), 'tmp');
488
+ fs.mkdirSync(tmpDir, { recursive: true });
489
+ const filePath = path.join(tmpDir, `memory-consolidation-${ts}.md`);
490
+ fs.writeFileSync(filePath, text, 'utf-8');
491
+ // ── 写批次目录(长期留存)────────────────────────────────────────────────
492
+ const contextFilePath = path.join(batchDir, 'context.md');
493
+ try {
494
+ fs.writeFileSync(contextFilePath, text, 'utf-8');
495
+ }
496
+ catch { /* 批次任务书写入失败不影响整理 */ }
497
+ return { filePath, batchDir, contextFilePath, scopes, historyJsonlPath, threadFiles, chatDir };
498
+ }
499
+ /** 会话结束后回填本次产出,追加到任务书末尾。 */
500
+ export function recordBatchOutcome(contextFilePath, outcome) {
501
+ try {
502
+ fs.appendFileSync(contextFilePath, `\n---\n\n## 本次产出\n\n${outcome}\n`, 'utf-8');
503
+ }
504
+ catch { /* ignore */ }
505
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * consolidation-ops-records.ts — 整理会话操作记录(v3)
3
+ *
4
+ * 记录整理会话内的所有关键事件:
5
+ * - session-start / session-end:会话级别的开始/结束记录
6
+ * - create / update / archive:每次写操作的结果
7
+ * - summary:会话结束时的汇总行
8
+ *
9
+ * session-end 写在 onConsolidationComplete 里,失败时可依据 ops.jsonl 逐条回撤。
10
+ */
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ /** 向 ops 记录文件追加一条记录。失败静默,不影响主流程。 */
14
+ export function appendOpsRecord(batchDir, entry) {
15
+ try {
16
+ const filePath = path.join(batchDir, 'ops.jsonl');
17
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
18
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8');
19
+ }
20
+ catch { /* ignore */ }
21
+ }
22
+ /**
23
+ * 扫描 ops 记录并返回汇总行(未写入文件)。
24
+ * 供会话结束时追加 summary 条目。
25
+ */
26
+ export function summarizeOpsRecords(batchDir) {
27
+ const summary = {
28
+ ts: new Date().toISOString(),
29
+ op: 'summary',
30
+ created: 0,
31
+ updated: 0,
32
+ archived: 0,
33
+ errors: 0,
34
+ };
35
+ const filePath = path.join(batchDir, 'ops.jsonl');
36
+ if (!fs.existsSync(filePath))
37
+ return summary;
38
+ try {
39
+ for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) {
40
+ const entry = JSON.parse(line);
41
+ if (entry.op === 'summary' || entry.op === 'session-start' || entry.op === 'session-end' || entry.op === 'feedback-set')
42
+ continue;
43
+ if (!entry.ok) {
44
+ summary.errors++;
45
+ continue;
46
+ }
47
+ if (entry.op === 'create')
48
+ summary.created++;
49
+ else if (entry.op === 'update')
50
+ summary.updated++;
51
+ else if (entry.op === 'archive')
52
+ summary.archived++;
53
+ }
54
+ }
55
+ catch { /* ignore */ }
56
+ return summary;
57
+ }
58
+ /**
59
+ * 读整理会话写入的反馈文本(`ec memory feedback`)。
60
+ * 取最后一条 feedback-set;没有则返回 undefined(收尾回退默认模板)。
61
+ */
62
+ export function readFeedbackText(batchDir) {
63
+ const filePath = path.join(batchDir, 'ops.jsonl');
64
+ if (!fs.existsSync(filePath))
65
+ return undefined;
66
+ 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
+ }
76
+ }
77
+ catch { /* ignore */ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * 收集 ops 记录里所有出现过的作用域及其 memory 根目录。
82
+ * 供 finishConsolidationSession 决定要对哪几个 scope 重建索引。
83
+ * 返回 Map<scope, memoryDir>;同一 scope 若出现多次取第一条(目录相同)。
84
+ */
85
+ export function collectAffectedScopes(batchDir) {
86
+ const result = new Map();
87
+ const filePath = path.join(batchDir, 'ops.jsonl');
88
+ if (!fs.existsSync(filePath))
89
+ return result;
90
+ try {
91
+ for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) {
92
+ const entry = JSON.parse(line);
93
+ if (entry.op !== 'create' && entry.op !== 'update' && entry.op !== 'archive')
94
+ continue;
95
+ const e = entry;
96
+ const s = e.scope;
97
+ if ((s === 'relation' || s === 'agent' || s === 'owner') && e.memoryDir && !result.has(s)) {
98
+ result.set(s, e.memoryDir);
99
+ }
100
+ }
101
+ }
102
+ catch { /* ignore */ }
103
+ return result;
104
+ }
105
+ /**
106
+ * 读取 ops 记录里所有成功的写操作,按写入顺序倒序返回(最后写的先回撤)。
107
+ * 只返回 create / update / archive 且 ok=true 的条目。
108
+ */
109
+ export function readRollbackRecords(batchDir) {
110
+ const filePath = path.join(batchDir, 'ops.jsonl');
111
+ if (!fs.existsSync(filePath))
112
+ return [];
113
+ try {
114
+ return fs.readFileSync(filePath, 'utf-8')
115
+ .split('\n')
116
+ .filter(Boolean)
117
+ .map(l => JSON.parse(l))
118
+ .filter((e) => (e.op === 'create' || e.op === 'update' || e.op === 'archive') && e.ok)
119
+ .reverse();
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ }