evolcore 0.0.33 → 0.0.34

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 (129) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +12 -1
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +82 -13
  6. package/dist/agents/codex/diagnostics.js +37 -11
  7. package/dist/agents/codex-runner.js +218 -44
  8. package/dist/agents/ecagent-runner.js +73 -20
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/identity.js +1 -1
  11. package/dist/channels/aun.js +16 -6
  12. package/dist/channels/daemon.js +6 -3
  13. package/dist/cli/aun-commands.js +3 -2
  14. package/dist/cli/ctl-command.js +3 -1
  15. package/dist/cli/daemon-commands.js +120 -19
  16. package/dist/cli/fs-command.js +2 -1
  17. package/dist/cli/index.js +20 -6
  18. package/dist/cli/integrity-command.js +196 -0
  19. package/dist/cli/memory-command.js +158 -145
  20. package/dist/cli/thread-command.js +5 -3
  21. package/dist/cli/trigger-command.js +6 -1
  22. package/dist/config/config-field-policy.js +4 -0
  23. package/dist/config/config-manager.js +19 -0
  24. package/dist/config/contact-book-store.js +5 -0
  25. package/dist/config/role-service.js +13 -0
  26. package/dist/config/runtime-env.js +16 -0
  27. package/dist/config-store.js +30 -0
  28. package/dist/core/audit/log-integrity.js +3 -1
  29. package/dist/core/auth/agent-delegation.js +108 -35
  30. package/dist/core/auth/delegation-diagnostics.js +414 -0
  31. package/dist/core/auth/operation-authorizer.js +1 -1
  32. package/dist/core/auth/operation-catalog.js +16 -0
  33. package/dist/core/command/agent-control.js +18 -26
  34. package/dist/core/command/command-handler.js +138 -4
  35. package/dist/core/command/daemon-diagnostics.js +1 -0
  36. package/dist/core/command/menu-catalog.js +3 -0
  37. package/dist/core/command/menu-handler.js +22 -7
  38. package/dist/core/command/menu-protocol.js +1 -0
  39. package/dist/core/command/slash-handler.js +2 -0
  40. package/dist/core/daemon-bootstrap.js +4 -7
  41. package/dist/core/file-integrity/read.js +61 -0
  42. package/dist/core/file-integrity/store.js +262 -0
  43. package/dist/core/ipc-registration.js +36 -1
  44. package/dist/core/memory/batch-meta.js +132 -0
  45. package/dist/core/memory/consolidation-context.js +35 -14
  46. package/dist/core/memory/consolidation-ops-records.js +14 -16
  47. package/dist/core/memory/consolidation.js +91 -26
  48. package/dist/core/memory/index-manager.js +19 -28
  49. package/dist/core/memory/memory-admin.js +19 -14
  50. package/dist/core/memory/memory-document.js +53 -9
  51. package/dist/core/memory/memory-writer.js +30 -15
  52. package/dist/core/message/im-renderer.js +20 -4
  53. package/dist/core/message/message-bridge.js +2 -1
  54. package/dist/core/message/response-engine.js +584 -47
  55. package/dist/core/permission/ec-command-parser.js +93 -4
  56. package/dist/core/permission/protected-paths.js +5 -1
  57. package/dist/core/permission/readonly-shell-query.js +43 -5
  58. package/dist/core/permission/session-path-grants.js +334 -0
  59. package/dist/core/permission/tool-canonical.js +226 -0
  60. package/dist/core/permission/tool-policy.js +30 -62
  61. package/dist/core/role/runtime-policy.js +21 -10
  62. package/dist/core/session/session-fs-store.js +123 -8
  63. package/dist/core/session/session-manager.js +75 -5
  64. package/dist/core/skill/index-generator.js +17 -3
  65. package/dist/core/system-session/grants.js +40 -0
  66. package/dist/core/system-session/memory.js +222 -0
  67. package/dist/core/system-session/registry.js +74 -0
  68. package/dist/core/system-session/types.js +14 -0
  69. package/dist/core/thread/thread-service.js +13 -20
  70. package/dist/eck/kit-renderer.js +7 -2
  71. package/dist/eck/manifest-engine.js +24 -4
  72. package/dist/index.js +11 -2
  73. package/dist/ipc.js +51 -40
  74. package/dist/paths.js +70 -0
  75. package/dist/stats/billing.js +20 -8
  76. package/dist/trigger/feedback.js +6 -1
  77. package/dist/trigger/parser.js +8 -1
  78. package/dist/trigger/system-definitions.js +3 -3
  79. package/dist/utils/cross-platform.js +11 -2
  80. package/dist/utils/error-dict.json +18 -4
  81. package/dist/utils/error-utils.js +121 -0
  82. package/dist/utils/instance-registry.js +13 -8
  83. package/dist/utils/log-writer.js +12 -0
  84. package/dist/utils/process-introspect.js +11 -0
  85. package/kits/docs/context-assembly.md +0 -1
  86. package/kits/docs/evolcore/agent-create.md +5 -4
  87. package/kits/docs/evolcore/agent.md +2 -2
  88. package/kits/docs/evolcore/config.md +2 -2
  89. package/kits/docs/evolcore/ctl.md +2 -2
  90. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  91. package/kits/docs/evolcore/fs-architecture.md +4 -5
  92. package/kits/docs/evolcore/fs.md +1 -1
  93. package/kits/docs/evolcore/group-collaboration.md +2 -2
  94. package/kits/docs/evolcore/group.md +10 -10
  95. package/kits/docs/evolcore/memory-architecture.md +4 -5
  96. package/kits/docs/evolcore/memory-consolidation.md +18 -14
  97. package/kits/docs/evolcore/memory-tags.md +3 -1
  98. package/kits/docs/evolcore/memory.md +24 -12
  99. package/kits/docs/evolcore/model.md +16 -0
  100. package/kits/docs/evolcore/msg.md +7 -7
  101. package/kits/docs/evolcore/thread-chain.md +5 -5
  102. package/kits/docs/prompt-loading-architecture.md +0 -1
  103. package/kits/eck_manifest.json +0 -20
  104. package/kits/eck_manifest.memory.json +0 -12
  105. package/kits/rules/01-overview.md +11 -0
  106. package/kits/rules/03-identity.md +6 -3
  107. package/kits/rules/06-channel.md +1 -1
  108. package/kits/rules/07-memory-and-thread.md +12 -15
  109. package/kits/schemas/_meta.json +7 -1
  110. package/kits/schemas/agent-config.schema.13.json +1 -1
  111. package/kits/schemas/daemon.schema.7.json +140 -0
  112. package/kits/skills/ec-group/SKILL.md +4 -4
  113. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  114. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  115. package/kits/templates/memory-consolidation/forget.md +2 -2
  116. package/kits/templates/memory-consolidation/generalize.md +4 -4
  117. package/kits/templates/memory-consolidation/notes.md +11 -11
  118. package/kits/templates/memory-consolidation/profile.md +3 -3
  119. package/kits/templates/memory-consolidation/promote.md +3 -3
  120. package/kits/templates/memory-consolidation/workflow.md +6 -6
  121. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  122. package/kits/templates/system-fragments/bootstrap.md +4 -3
  123. package/kits/templates/system-fragments/channel.md +2 -2
  124. package/kits/templates/system-fragments/memory.md +16 -9
  125. package/kits/templates/system-fragments/reply.md +6 -6
  126. package/kits/templates/system-fragments/session.md +1 -1
  127. package/package.json +4 -3
  128. package/dist/core/memory/memory-session-policy.js +0 -41
  129. package/dist/core/memory/memory-signature.js +0 -257
@@ -12,35 +12,35 @@ 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 { ensureSessionRuntimeDir } from '../../cli/task-context.js';
15
17
  import { formatChannelKey } from '../channel-loader.js';
16
18
  import { logger } from '../../utils/logger.js';
17
- import { appendOpsRecord } from './consolidation-ops-records.js';
19
+ import { appendOpsRecord, feedbackFilePath, readFeedbackText } from './consolidation-ops-records.js';
20
+ import { updateBatchMeta } from './batch-meta.js';
18
21
  import { checkConsolidationCandidates, shouldUseMetaIndex } from './consolidation-trigger.js';
19
22
  /**
20
- * 清理当前 agent 的旧整理批次。
21
- * 启动新整理时调用,保留最新的一个会话历史用于查看和调试。
23
+ * 清理旧整理批次,全局保留最新的 10 个(不区分 aid 或 mode)。
24
+ * 启动新整理时调用;selfAid 参数保留供日志上下文用,不参与过滤。
22
25
  */
23
- function cleanupPreviousConsolidationBatches(selfAid) {
26
+ function cleanupPreviousConsolidationBatches(_selfAid) {
24
27
  try {
25
28
  const batchRoot = path.join(resolveRoot(), 'memory', 'consolidation-batches');
26
29
  if (!fs.existsSync(batchRoot))
27
30
  return;
28
- // 匹配格式:YYYYMMDDTHHMMSS-<selfAid>-*
29
- const escapedAid = selfAid.replace(/\./g, '\\.');
30
- const pattern = new RegExp(`^\\d{8}T\\d{6}-${escapedAid}-`);
31
31
  const entries = fs.readdirSync(batchRoot, { withFileTypes: true })
32
- .filter(e => e.isDirectory() && pattern.test(e.name))
32
+ .filter(e => e.isDirectory())
33
33
  .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++) {
34
+ .sort((a, b) => b.name.localeCompare(a.name)); // 时间戳前缀,字典序即时间序,最新的在前
35
+ const KEEP = 10;
36
+ for (let i = KEEP; i < entries.length; i++) {
37
37
  const dir = entries[i];
38
38
  try {
39
39
  fs.rmSync(dir.path, { recursive: true, force: true });
40
- logger.debug(`[Memory] 清理前一次整理批次: ${dir.name}`);
40
+ logger.debug(`[Memory] 清理旧整理批次: ${dir.name}`);
41
41
  }
42
42
  catch (e) {
43
- logger.warn(`[Memory] 清理前一次整理批次失败 ${dir.name}: ${e?.message}`);
43
+ logger.warn(`[Memory] 清理旧整理批次失败 ${dir.name}: ${e?.message}`);
44
44
  }
45
45
  }
46
46
  }
@@ -60,6 +60,10 @@ export async function launchConsolidation(channel, params) {
60
60
  const selfAid = effectiveCtx.selfAid;
61
61
  if (!selfAid)
62
62
  throw new Error('无法确定当前 agent AID(selfAid 为空)');
63
+ // 系统会话标记:类型 / subtype 必须已注册,runner 必须受支持(Gemini 拒绝)。
64
+ // 放在认领 processing 之前,拒绝时不留副作用。
65
+ const systemSession = { type: 'memory', subtype: mode };
66
+ assertSystemSessionLaunchable(systemSession, effectiveCtx.baseagent ?? 'claude');
63
67
  // 清理旧的整理批次(保留最新的一个历史)
64
68
  cleanupPreviousConsolidationBatches(selfAid);
65
69
  // notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
@@ -96,7 +100,6 @@ export async function launchConsolidation(channel, params) {
96
100
  triggeredBy,
97
101
  taskCtx: effectiveCtx,
98
102
  ownerAid,
99
- sessionRuntimeDir: effectiveCtx.sessionRuntimeDir,
100
103
  ownerInvolved: params.ownerInvolved,
101
104
  forgetAfterDays: params.forgetAfterDays,
102
105
  scopeOverride: params.scopeOverride,
@@ -147,6 +150,8 @@ export async function launchConsolidation(channel, params) {
147
150
  projectPath,
148
151
  prompt,
149
152
  metadata: {
153
+ // 系统会话的唯一判据:路径 / 工具 / ec 命令由 system-session/memory.ts 决定,与角色无关
154
+ systemSession,
150
155
  sessionType: 'memory',
151
156
  memoryMode: mode,
152
157
  ...(params.scopeOverride ? { memoryScope: params.scopeOverride } : {}),
@@ -154,6 +159,7 @@ export async function launchConsolidation(channel, params) {
154
159
  consolidationBatchDir: ctxFile.batchDir,
155
160
  ...(ctxFile.chatDir ? { consolidationChatDir: ctxFile.chatDir } : {}),
156
161
  ...(claimedProcessing && memoryDir ? { consolidationMemoryDir: memoryDir } : {}),
162
+ ...(ownerAid ? { consolidationOwnerAid: ownerAid } : {}),
157
163
  consolidationUsedThreadSnapshots: !!ctxFile.threadFiles?.length,
158
164
  ...(ctxFile.snapshotThroughTs ? { consolidationSnapshotThroughTs: ctxFile.snapshotThroughTs } : {}),
159
165
  ...(effectiveCtx.peerKey ? { peerKey: effectiveCtx.peerKey } : {}),
@@ -380,9 +386,8 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
380
386
  for (const [scope, memoryDir] of affectedScopes) {
381
387
  const consolidatedDir = path.join(memoryDir, 'consolidated');
382
388
  const indexPath = path.join(memoryDir, 'MEMORY.md');
383
- const invalidDir = path.join(memoryDir, 'archive', 'invalid');
384
389
  try {
385
- await rebuildIndex(consolidatedDir, indexPath, scope, invalidDir, undefined, maxBytes);
390
+ await rebuildIndex(consolidatedDir, indexPath, scope, maxBytes);
386
391
  }
387
392
  catch { }
388
393
  }
@@ -464,12 +469,28 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
464
469
  fs.writeFileSync(historyPath, JSON.stringify({ note: `runner ${baseagent} does not support getSessionMessages` }) + '\n', 'utf-8');
465
470
  }
466
471
  }
472
+ // feedback 只读一次:先落 batch.md,再清 scratch/,投递复用同一份文本
473
+ const customText = readFeedbackText(batchDir);
474
+ // batch.md 收尾更新:写入最终状态和 feedback 正文
475
+ try {
476
+ updateBatchMeta(batchDir, {
477
+ status: success ? 'completed' : 'failed',
478
+ finishedAt: new Date().toISOString(),
479
+ success,
480
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
481
+ feedbackText: customText,
482
+ });
483
+ }
484
+ catch { /* 失败静默 */ }
485
+ // scratch/ 是会话期临时工作区,feedback 已落 batch.md,不分成败与触发方式一律清掉
486
+ try {
487
+ fs.rmSync(path.dirname(feedbackFilePath(batchDir)), { recursive: true, force: true });
488
+ }
489
+ catch { /* 静默,不影响投递 */ }
467
490
  const feedbackOrigin = meta.feedbackOrigin;
468
491
  const triggeredBy = meta.consolidationTriggeredBy;
469
492
  if (feedbackOrigin && triggeredBy && triggeredBy !== 'periodic-trigger' && session.selfAID) {
470
493
  try {
471
- const { readFeedbackText } = await import('./consolidation-ops-records.js');
472
- const customText = readFeedbackText(batchDir);
473
494
  const text = customText ?? renderDefaultConsolidationFeedback({
474
495
  mode: meta.memoryMode,
475
496
  triggeredBy,
@@ -490,6 +511,21 @@ export function createConsolidationSessionFinalizer({ sessionManager, getAgent,
490
511
  finally {
491
512
  if (releaseAid && sessionId)
492
513
  releaseSlot(releaseAid, sessionId);
514
+ // runtime 目录清理:immediate 策略下,finalizer 是最后一个要读它的消费者。
515
+ // 用 sessionId 无状态重算路径,不依赖 spec 传递——保证即使 spec 不传 runtimeDir
516
+ // 字段,目录也能在会话结束后被清掉。
517
+ if (sessionId) {
518
+ const runtimeDir = ensureSessionRuntimeDir(sessionId);
519
+ if (runtimeDir) {
520
+ try {
521
+ fs.rmSync(runtimeDir, { recursive: true, force: true });
522
+ logger.debug(`[Memory] 清理 runtime 目录: ${runtimeDir}`);
523
+ }
524
+ catch (cleanupError) {
525
+ logger.warn(`[Memory] runtime 目录清理失败(session=${sessionId}): ${cleanupError?.message}`);
526
+ }
527
+ }
528
+ }
493
529
  }
494
530
  };
495
531
  }
@@ -514,19 +550,48 @@ export function createConsolidationFeedbackSender(getChannel) {
514
550
  replyContext.threadId = threadId;
515
551
  if (chatType)
516
552
  replyContext.delivery = { chatType };
517
- await binding.adapter.send(buildEnvelope({
518
- taskId: `memory-feedback-${Date.now()}`,
553
+ // 对比期:同一份反馈 notice、text 各发一条,观察两种模式在对端的差异,
554
+ // 跑通后删掉 text 那条。两条 taskId 后缀不同,避免共用 operationId 被出站去重吞掉一条;
555
+ // 各自 try,一条失败不连累另一条。
556
+ const stamp = Date.now();
557
+ const envelopeFor = (suffix) => buildEnvelope({
558
+ taskId: `memory-feedback-${stamp}-${suffix}`,
519
559
  channel: binding.adapter.channelKey,
520
560
  channelId,
521
561
  agentName: binding.agentName,
522
562
  replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
523
- }), {
524
- kind: 'result.text',
525
- text,
526
- isFinal: true,
527
- format: 'markdown',
528
563
  });
529
- logger.info(`[Memory] 整理反馈已投递(aid=${selfAid},channelId=${channelId})`);
564
+ const attempts = [
565
+ {
566
+ mode: 'notice',
567
+ send: () => binding.adapter.send(envelopeFor('notice'), {
568
+ kind: 'system.notice',
569
+ text,
570
+ subtype: 'memory.consolidation.result',
571
+ }),
572
+ },
573
+ {
574
+ mode: 'text',
575
+ send: () => binding.adapter.send(envelopeFor('text'), {
576
+ kind: 'result.text',
577
+ text,
578
+ isFinal: true,
579
+ format: 'markdown',
580
+ }),
581
+ },
582
+ ];
583
+ const failures = [];
584
+ for (const { mode, send } of attempts) {
585
+ try {
586
+ await send();
587
+ logger.info(`[Memory] 整理反馈已投递(mode=${mode},aid=${selfAid},channelId=${channelId})`);
588
+ }
589
+ catch (error) {
590
+ failures.push(`${mode}: ${error?.message}`);
591
+ }
592
+ }
593
+ if (failures.length > 0)
594
+ throw new Error(failures.join('; '));
530
595
  };
531
596
  }
532
597
  export function registerConsolidationSessionEvents({ eventBus, permissionGateway, finalize, }) {
@@ -2,7 +2,7 @@
2
2
  * index-manager.ts — MEMORY.md 索引管理(v3)
3
3
  *
4
4
  * 负责:
5
- * 1. 重建索引:扫描 consolidated/,逐份验签,按 tags 筛选,截断到 indexMaxBytes
5
+ * 1. 重建索引:扫描 consolidated/,逐份过台账校验,按 tags 筛选,截断到 indexMaxBytes
6
6
  * 2. 追加条目:单条写入时的快速路径(写入后仍以 rebuild 为准)
7
7
  *
8
8
  * 排序与超期判断一律读**文件 mtime**;frontmatter 的 updatedAt 只作可读信息。
@@ -10,15 +10,16 @@
10
10
  * 索引行格式:`- 20261005 [slug] — 描述 [tag1,tag2]`
11
11
  * slug 即文件名(consolidated/<slug>.md),不再重复写路径。
12
12
  *
13
- * 验签规则(v3 收紧):只认 `verified`。
14
- * - `invalid`(签名不符)→ 移入 archive/invalid/,不进索引
15
- * - `unsigned`(没有签名块)→ 同样移入 archive/invalid/,不进索引
16
- * 主会话和整理会话都写不到 consolidated/(create 由程序签名),出现未签名文件
17
- * 说明有人绕过写入通道直接落盘。
13
+ * 完整性校验走 file-integrity 台账(docs/file-security/):
14
+ * - 没被动过 → 进索引
15
+ * - 被带外修改 → 恢复到最后登记的版本,再进索引
16
+ * - 被改动且无法恢复 → 跳过,不进索引,也不移动文件
17
+ * MEMORY.md 本身每次写入都会登记:它整份注入上下文,被改了同样要能恢复。
18
18
  */
19
19
  import fs from 'fs';
20
20
  import path from 'path';
21
21
  import { getMemoryArray, getMemoryString, parseMemoryDocument } from './memory-document.js';
22
+ import { registerFile, verifyFile } from '../file-integrity/store.js';
22
23
  /** 索引容量上限(字节)。实际取值由调用方从 memory.indexMaxBytes 传入。 */
23
24
  export const DEFAULT_INDEX_MAX_BYTES = 5 * 1024;
24
25
  /** Fast metadata index for consolidation candidate scans. */
@@ -246,20 +247,19 @@ function writeIndexFile(indexPath, content) {
246
247
  const tmp = indexPath + '.tmp';
247
248
  fs.writeFileSync(tmp, content, 'utf-8');
248
249
  fs.renameSync(tmp, indexPath);
250
+ registerFile(indexPath);
249
251
  }
250
252
  // ── 重建索引 ────────────────────────────────────────────────────────────────
251
253
  /**
252
254
  * 从 consolidated/ 重建 MEMORY.md。
253
255
  *
254
- * verifyFn 可选。不传时所有文件视为通过,直接进索引,不移动任何文件。
255
- * 传入时验签失败的文件只跳过(不进索引),同样不移动文件——移动行为是验签门控
256
- * 开启后才需要的,暂未实现。排序读文件 mtime,按 tags 分级,超 indexMaxBytes
257
- * 从末尾截断(pinned 标签保留)。
256
+ * 每份文件先过台账校验(被带外修改的先恢复),被改动且无法恢复的跳过,不移动文件。
257
+ * 排序读文件 mtime,按 tags 分级,超 indexMaxBytes 从末尾截断(pinned 标签保留)。
258
258
  */
259
- export async function rebuildIndex(consolidatedDir, indexPath, scope, invalidDir, verifyFn, indexMaxBytes = DEFAULT_INDEX_MAX_BYTES) {
259
+ export async function rebuildIndex(consolidatedDir, indexPath, scope, indexMaxBytes = DEFAULT_INDEX_MAX_BYTES) {
260
260
  if (!fs.existsSync(consolidatedDir)) {
261
261
  writeIndexFile(indexPath, serializeIndex(scope, []));
262
- return { indexed: 0, invalidMoved: 0, verifiedFiles: [] };
262
+ return { indexed: 0, verifiedFiles: [] };
263
263
  }
264
264
  const files = fs.readdirSync(consolidatedDir).filter(f => f.endsWith('.md')).sort();
265
265
  const entries = [];
@@ -269,26 +269,17 @@ export async function rebuildIndex(consolidatedDir, indexPath, scope, invalidDir
269
269
  let content;
270
270
  let mtimeMs;
271
271
  try {
272
- content = fs.readFileSync(filePath, 'utf-8');
272
+ const checked = verifyFile(filePath, { restoreOnFail: true });
273
+ if (!checked.bytes)
274
+ continue; // missing / 无法恢复:不进索引
275
+ content = checked.bytes.toString('utf-8');
276
+ // 恢复会把 mtime 还原成登记时的值,所以在校验之后再取
273
277
  mtimeMs = fs.statSync(filePath).mtimeMs;
274
278
  }
275
279
  catch {
276
280
  continue;
277
281
  }
278
- if (verifyFn) {
279
- const verifyResult = await verifyFn(content);
280
- if (verifyResult.status === 'verified') {
281
- verifiedFiles.push(file);
282
- }
283
- else {
284
- // 验签失败:跳过,不进索引,不移动文件(门控开启后的移动逻辑暂未实现)
285
- continue;
286
- }
287
- }
288
- else {
289
- // 未开启验签:所有文件视为通过
290
- verifiedFiles.push(file);
291
- }
282
+ verifiedFiles.push(file);
292
283
  const document = parseMemoryDocument(content);
293
284
  const description = getMemoryString(document, 'description') ?? file.replace(/\.md$/, '');
294
285
  const tags = getMemoryArray(document, 'tags');
@@ -304,5 +295,5 @@ export async function rebuildIndex(consolidatedDir, indexPath, scope, invalidDir
304
295
  sortEntries(entries);
305
296
  const kept = truncateToLimit(scope, entries, indexMaxBytes);
306
297
  writeIndexFile(indexPath, serializeIndex(scope, kept));
307
- return { indexed: kept.length, invalidMoved: 0, verifiedFiles };
298
+ return { indexed: kept.length, verifiedFiles };
308
299
  }
@@ -12,7 +12,8 @@ import path from 'path';
12
12
  import { resolveScopeDirs, archiveMemory, dropMemoryNote, listMemoryNotes } from './memory-writer.js';
13
13
  import { rebuildIndex, DEFAULT_INDEX_MAX_BYTES } from './index-manager.js';
14
14
  import { refreshMemoryMeta, readMemoryMeta } from './index-manager.js';
15
- import { parseSignatureBlock, verifyMemoryFile } from './memory-signature.js';
15
+ import { stripLegacySignature } from './memory-document.js';
16
+ import { moveRecord, verifyFile } from '../file-integrity/store.js';
16
17
  import { readFmArray, readFmValue, readUpdatedAtMs, reviewWindowDays, DEFAULT_FORGET_AFTER_DAYS } from './staleness.js';
17
18
  export const MEMORY_SCOPES = ['relation', 'agent', 'owner'];
18
19
  const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$/;
@@ -45,10 +46,6 @@ function fmtDate(ms) {
45
46
  return '-';
46
47
  return new Date(ms).toISOString().slice(0, 16).replace('T', ' ');
47
48
  }
48
- /** 去掉签名块,只留正文(含 frontmatter) */
49
- function stripSignature(content) {
50
- return parseSignatureBlock(content).payload.trimEnd();
51
- }
52
49
  // ── 查看 ────────────────────────────────────────────────────────────────────
53
50
  /** 概览:各作用域条数、pending 数、索引大小、最近更新时间。 */
54
51
  export function memoryOverview(target, scopes) {
@@ -120,7 +117,6 @@ export function listMemoryFiles(target, scope, archived = false, now = Date.now(
120
117
  tags,
121
118
  mtimeMs,
122
119
  updatedAtMs,
123
- signed: !!parseSignatureBlock(content).fields,
124
120
  reviewInDays: Math.floor((updatedAtMs + windowMs - now) / DAY_MS),
125
121
  });
126
122
  }
@@ -135,7 +131,7 @@ export function formatMemoryFiles(scope, files, archived = false) {
135
131
  for (const f of files) {
136
132
  const tags = f.tags.length ? ` [${f.tags.join(',')}]` : '';
137
133
  const review = archived ? '' : f.reviewInDays < 0 ? ' ⏳待审查' : ` ${f.reviewInDays}天后审查`;
138
- lines.push(`- ${f.slug}${tags}${f.signed ? '' : ' ⚠️未签名'}${review}`);
134
+ lines.push(`- ${f.slug}${tags}${review}`);
139
135
  if (f.description)
140
136
  lines.push(` ${f.description}`);
141
137
  lines.push(` 更新于 ${fmtDate(f.updatedAtMs)}`);
@@ -158,16 +154,21 @@ export async function showMemoryFile(target, scope, slug, archived = false) {
158
154
  const fp = path.join(dir, `${slug}.md`);
159
155
  if (!fs.existsSync(fp))
160
156
  throw new Error(`找不到记忆: ${scope}/${archived ? 'archive/' : ''}${slug}`);
161
- const content = fs.readFileSync(fp, 'utf-8');
162
157
  let verify;
158
+ let content;
163
159
  try {
164
- const r = await verifyMemoryFile(content, target.selfAid);
165
- verify = r.status === 'verified' ? '✅ 签名有效' : r.status === 'unsigned' ? '⚠️ 未签名' : `❌ 签名无效(${r.reason ?? 'unknown'})`;
160
+ const r = verifyFile(fp, { restoreOnFail: true });
161
+ verify = r.status === 'verified' ? '✅ 校验通过'
162
+ : r.status === 'restored' ? '♻️ 检测到带外修改,已恢复到最后登记版本'
163
+ : '❌ 被改动且无法恢复';
164
+ // tampered 时没有校验过的字节,显示磁盘现状供排查
165
+ content = r.bytes ? r.bytes.toString('utf-8') : fs.readFileSync(fp, 'utf-8');
166
166
  }
167
167
  catch (e) {
168
- verify = `⚠️ 无法验签(${e?.message || String(e)})`;
168
+ verify = `⚠️ 无法校验(${e?.message || String(e)})`;
169
+ content = fs.readFileSync(fp, 'utf-8');
169
170
  }
170
- let body = stripSignature(content);
171
+ let body = stripLegacySignature(content).trimEnd();
171
172
  if (body.length > MAX_SHOW_CHARS)
172
173
  body = `${body.slice(0, MAX_SHOW_CHARS)}\n…(已截断)`;
173
174
  return `📄 ${scope}/${archived ? 'archive/' : ''}${slug} ${verify}\n${body}`;
@@ -201,8 +202,7 @@ export function showPendingNote(target, slug) {
201
202
  /** 重建某作用域的 MEMORY.md 并刷新 _meta.json。 */
202
203
  export async function reindexScope(target, scope) {
203
204
  const dirs = scopeDirs(target, scope);
204
- const r = await rebuildIndex(dirs.consolidatedDir, path.join(dirs.memoryDir, 'MEMORY.md'), scope, path.join(dirs.memoryDir, 'archive', 'invalid'), undefined, // 与 ipc 一致:验签门控尚未开启
205
- target.memoryConfig?.indexMaxBytes ?? DEFAULT_INDEX_MAX_BYTES);
205
+ const r = await rebuildIndex(dirs.consolidatedDir, path.join(dirs.memoryDir, 'MEMORY.md'), scope, target.memoryConfig?.indexMaxBytes ?? DEFAULT_INDEX_MAX_BYTES);
206
206
  refreshMemoryMeta(dirs.memoryDir);
207
207
  return { indexed: r.indexed };
208
208
  }
@@ -228,7 +228,12 @@ export async function restoreMemoryFile(target, scope, slug) {
228
228
  throw new Error(`找不到已归档记忆: ${scope}/${slug}`);
229
229
  if (fs.existsSync(dest))
230
230
  throw new Error(`正式记忆中已存在同名文件: ${scope}/${slug},未恢复`);
231
+ // 归档期间被改过的话先恢复;恢复不了就拒绝,不把篡改内容放回正式记忆
232
+ const checked = verifyFile(src, { restoreOnFail: true });
233
+ if (!checked.bytes)
234
+ throw new Error(`MEMORY_TAMPERED: 已归档记忆被改动且无法恢复,未恢复: ${scope}/${slug}`);
231
235
  fs.mkdirSync(dirs.consolidatedDir, { recursive: true });
232
236
  fs.renameSync(src, dest);
237
+ moveRecord(src, dest);
233
238
  await reindexScope(target, scope);
234
239
  }
@@ -2,10 +2,58 @@
2
2
  * Shared representation for memory Markdown documents.
3
3
  *
4
4
  * Memory files have two mutable blocks: YAML-like frontmatter and Markdown
5
- * body. AUN signature blocks are stripped while parsing and are never exposed
6
- * as a document block; consolidated writers sign the serialized result again.
5
+ * body. 新记忆不再签名(完整性改由 file-integrity 台账保证);磁盘上的老记忆
6
+ * 可能还带尾部 AUN 签名块,解析时剥掉,重写时不再写回。
7
7
  */
8
- import { parseSignatureBlock } from './memory-signature.js';
8
+ // ── 老签名块兼容剥离 ──────────────────────────────────────────────────────────
9
+ // 规则与原 memory-signature.ts 的 parseSignatureBlock 一致:只有格式完全合法的
10
+ // 签名块才剥;格式不对的标记可能就是正文,原样保留每个字节。
11
+ const LEGACY_SIGNATURE_MARKER = '<!-- AUN-SIGNATURE';
12
+ const LEGACY_SIGNATURE_RE = /^<!-- AUN-SIGNATURE(?:\r\n|\n|\r)(?<body>[\s\S]*?)(?:\r\n|\n|\r)-->\s*$/;
13
+ const LEGACY_REQUIRED_FIELDS = ['cert_fingerprint', 'timestamp', 'signature'];
14
+ const LEGACY_ALLOWED_FIELDS = [...LEGACY_REQUIRED_FIELDS, 'public_key_fingerprint'];
15
+ const SHA256_FP_RE = /^sha256:[0-9a-f]{64}$/i;
16
+ function isStrictBase64(value) {
17
+ if (!value || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value))
18
+ return false;
19
+ return Buffer.from(value, 'base64').toString('base64') === value;
20
+ }
21
+ function isValidLegacySignatureBlock(tail) {
22
+ const match = tail.match(LEGACY_SIGNATURE_RE);
23
+ if (!match)
24
+ return false;
25
+ const fields = {};
26
+ for (const rawLine of (match.groups?.body ?? '').split(/\r\n|\n|\r/)) {
27
+ const line = rawLine.trim();
28
+ if (!line)
29
+ continue;
30
+ const colon = line.indexOf(':');
31
+ if (colon < 0)
32
+ return false;
33
+ const key = line.slice(0, colon).trim().toLowerCase();
34
+ if (Object.prototype.hasOwnProperty.call(fields, key))
35
+ return false;
36
+ fields[key] = line.slice(colon + 1).trim();
37
+ }
38
+ if (LEGACY_REQUIRED_FIELDS.some(k => !fields[k]))
39
+ return false;
40
+ if (Object.keys(fields).some(k => !LEGACY_ALLOWED_FIELDS.includes(k)))
41
+ return false;
42
+ if (!SHA256_FP_RE.test(fields.cert_fingerprint))
43
+ return false;
44
+ if (fields.public_key_fingerprint && !SHA256_FP_RE.test(fields.public_key_fingerprint))
45
+ return false;
46
+ return /^\d+$/.test(fields.timestamp) && isStrictBase64(fields.signature);
47
+ }
48
+ /** 去掉老记忆尾部合法的 AUN 签名块;没有或不合法时原样返回。 */
49
+ export function stripLegacySignature(content) {
50
+ const idx = content.lastIndexOf(LEGACY_SIGNATURE_MARKER);
51
+ if (idx < 0 || content.indexOf(LEGACY_SIGNATURE_MARKER) !== idx)
52
+ return content;
53
+ if (idx > 0 && content[idx - 1] !== '\n' && content[idx - 1] !== '\r')
54
+ return content;
55
+ return isValidLegacySignatureBlock(content.slice(idx)) ? content.slice(0, idx) : content;
56
+ }
9
57
  function parseArray(raw) {
10
58
  const value = raw.match(/^\[([^\]]*)\]$/)?.[1] ?? raw;
11
59
  return value
@@ -29,13 +77,9 @@ function splitDocument(content) {
29
77
  hasFrontmatter: true,
30
78
  };
31
79
  }
32
- /** Parse frontmatter and body; an existing AUN signature is ignored. */
80
+ /** Parse frontmatter and body; a legacy AUN signature block is ignored. */
33
81
  export function parseMemoryDocument(content) {
34
- const signature = parseSignatureBlock(content);
35
- // A malformed marker may be ordinary body text. Only strip a signature when
36
- // the signature parser accepted the block; otherwise preserve every byte.
37
- const unsigned = signature.fields ? signature.payload : content;
38
- const parts = splitDocument(unsigned);
82
+ const parts = splitDocument(stripLegacySignature(content));
39
83
  const fields = {};
40
84
  const fieldOrder = [];
41
85
  const extraFrontmatterLines = [];
@@ -4,21 +4,22 @@
4
4
  * 只有整理会话(sessionType=memory)能调用这三个操作,由 ipc.ts 做会话类型与
5
5
  * 模式↔作用域的双重校验后委托到这里。本模块只负责文件层面的动作:
6
6
  *
7
- * - createMemory 新建正式记忆:写 consolidated/、签名、归档 --replaces 的旧文件
8
- * - updateMemory 改标签或续期(--touch),改完重新签名
7
+ * - createMemory 新建正式记忆:写 consolidated/、归档 --replaces 的旧文件
8
+ * - updateMemory 改标签或续期(--touch)
9
9
  * - archiveMemory 归档(唯一的下线方式,没有删除)
10
10
  *
11
- * 签名基础设施已就绪(signMemoryFile),但验签门控尚未开启——
12
- * 门控设计已存档,待后续按需启用。
11
+ * 正式记忆的完整性由 file-integrity 台账保证(docs/file-security/):
12
+ * 落盘后 registerFile,rename 后 moveRecord,读改写之前先 verifyFile。
13
+ * pending note 是草稿,不进台账。
13
14
  */
14
15
  import fs from 'fs';
15
16
  import path from 'path';
16
17
  import { resolveRoot } from '../../paths.js';
17
- import { signMemoryFile } from './memory-signature.js';
18
+ import { moveRecord, registerFile, verifyFile } from '../file-integrity/store.js';
18
19
  import { refreshMemoryMeta } from './index-manager.js';
19
20
  import { getMemoryArray, getMemoryString, parseMemoryDocument, serializeMemoryDocument, setMemoryField, } from './memory-document.js';
20
21
  const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$/;
21
- /** 与 memory-signature.ts 的 MAX_MEMORY_BYTES 保持一致(10KB)。 */
22
+ /** 一条记忆应是一句话能说清的结论,10KB 足够;超过通常意味着该拆成多条。 */
22
23
  const MAX_CONTENT_BYTES = 10 * 1024;
23
24
  /** 解析某个作用域的记忆目录。 */
24
25
  export function resolveScopeDirs(scope, taskCtx, ownerAid, peerKeyOverride) {
@@ -76,8 +77,18 @@ function assertSlug(slug) {
76
77
  function readMemory(content) {
77
78
  return parseMemoryDocument(content);
78
79
  }
79
- function writeSignedMemory(document, selfAid) {
80
- return signMemoryFile(serializeMemoryDocument(document), selfAid).then(result => result.signed);
80
+ /**
81
+ * 读 → 改 → 重写之前先过台账:被带外改过就先恢复,用校验过的内容做修改基础。
82
+ * 直接读盘的话,篡改内容改一下就会被重新登记成新基线。
83
+ */
84
+ function readVerifiedMemory(filePath) {
85
+ const r = verifyFile(filePath, { restoreOnFail: true });
86
+ if (r.status === 'missing')
87
+ throw new Error(`找不到记忆文件: ${filePath}`);
88
+ if (!r.bytes) {
89
+ throw new Error(`MEMORY_TAMPERED: 记忆文件被改动且无法恢复,已拒绝操作: ${filePath}`);
90
+ }
91
+ return r.bytes.toString('utf-8');
81
92
  }
82
93
  export async function createMemory(req) {
83
94
  assertSlug(req.slug);
@@ -134,10 +145,10 @@ export async function createMemory(req) {
134
145
  if (replacePaths.length > 0)
135
146
  setMemoryField(document, 'replaces', replacePaths.map(p => path.relative(dirs.memoryDir, p)));
136
147
  document.body = req.content.trimEnd();
137
- const signed = await writeSignedMemory(document, req.taskCtx.selfAid);
138
148
  const tmp = targetPath + '.tmp';
139
- fs.writeFileSync(tmp, signed, 'utf-8');
149
+ fs.writeFileSync(tmp, serializeMemoryDocument(document), 'utf-8');
140
150
  fs.renameSync(tmp, targetPath);
151
+ registerFile(targetPath, req.taskCtx.selfAid);
141
152
  // 归档被替换的旧文件(在写入成功之后做,失败不影响新文件)
142
153
  const archived = [];
143
154
  for (const p of replacePaths) {
@@ -150,6 +161,7 @@ export async function createMemory(req) {
150
161
  try {
151
162
  fs.renameSync(p, dest);
152
163
  archived.push(dest);
164
+ moveRecord(p, dest);
153
165
  }
154
166
  catch { /* 归档失败不影响主流程,由下次整理清理 */ }
155
167
  }
@@ -206,7 +218,8 @@ export async function demoteMemory(req) {
206
218
  const srcPath = path.join(srcDirs.consolidatedDir, `${req.slug}.md`);
207
219
  if (!fs.existsSync(srcPath))
208
220
  throw new Error(`找不到记忆文件: ${srcPath}`);
209
- const content = fs.readFileSync(srcPath, 'utf-8');
221
+ // 降级目标由 frontmatter 决定,必须读校验过的内容,否则改一下来源字段就能把记忆搬去别处
222
+ const content = readVerifiedMemory(srcPath);
210
223
  const fromRelPath = getMemoryString(readMemory(content), req.scope === 'agent' ? 'generalizedFrom' : 'promotedFrom');
211
224
  if (!fromRelPath) {
212
225
  throw new Error(`DEMOTE_NO_SOURCE: 记忆文件缺少来源字段(${req.scope === 'agent' ? 'generalizedFrom' : 'promotedFrom'}),` +
@@ -229,12 +242,14 @@ export async function demoteMemory(req) {
229
242
  const archiveDest = archivePath(srcDirs.memoryDir, srcPath);
230
243
  fs.mkdirSync(path.dirname(archiveDest), { recursive: true });
231
244
  fs.renameSync(srcPath, archiveDest);
245
+ moveRecord(srcPath, archiveDest);
232
246
  finalDestPath = archiveDest;
233
247
  conflictArchived = true;
234
248
  }
235
249
  else {
236
250
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
237
251
  fs.renameSync(srcPath, destPath);
252
+ moveRecord(srcPath, destPath);
238
253
  }
239
254
  // 跨作用域搬运,源与目标两个目录都要刷新元数据
240
255
  refreshMemoryMeta(srcDirs.memoryDir);
@@ -252,18 +267,17 @@ export async function updateMemory(req) {
252
267
  const filePath = resolveConsolidatedPath(dirs, req.target);
253
268
  if (!fs.existsSync(filePath))
254
269
  throw new Error(`找不到记忆文件: ${filePath}`);
255
- const raw = fs.readFileSync(filePath, 'utf-8');
256
- const document = readMemory(raw);
270
+ const document = readMemory(readVerifiedMemory(filePath));
257
271
  const prevTags = getMemoryArray(document, 'tags');
258
272
  if (req.tags !== undefined)
259
273
  setMemoryField(document, 'tags', req.tags.length > 0 ? req.tags : undefined);
260
274
  // 刷新 updatedAt(--touch 的续期语义就是刷新这里 + 文件 mtime)
261
275
  const now = new Date().toISOString();
262
276
  setMemoryField(document, 'updatedAt', now);
263
- const signed = await writeSignedMemory(document, req.taskCtx.selfAid);
264
277
  const tmp = filePath + '.tmp';
265
- fs.writeFileSync(tmp, signed, 'utf-8');
278
+ fs.writeFileSync(tmp, serializeMemoryDocument(document), 'utf-8');
266
279
  fs.renameSync(tmp, filePath);
280
+ registerFile(filePath, req.taskCtx.selfAid);
267
281
  refreshMemoryMeta(dirs.memoryDir);
268
282
  return { ok: true, filePath, prevTags, scopeDirs: dirs };
269
283
  }
@@ -278,6 +292,7 @@ export async function archiveMemory(req) {
278
292
  const dest = archivePath(dirs.memoryDir, p);
279
293
  fs.mkdirSync(path.dirname(dest), { recursive: true });
280
294
  fs.renameSync(p, dest);
295
+ moveRecord(p, dest);
281
296
  archived.push(dest);
282
297
  }
283
298
  refreshMemoryMeta(dirs.memoryDir);
@@ -1,6 +1,6 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { summarizeToolInput, summarizeToolInputForDisplay, toolInputForDisplay } from '../../utils/tool-summary.js';
3
- import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
3
+ import { getErrorMessage, getToolResultErrorText, isContextTooLongText } from '../../utils/error-utils.js';
4
4
  import { tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
5
5
  import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
6
6
  import { classifyAunToolErrorCode, normalizeToolErrorClassification } from '../permission/tool-error-code.js';
@@ -275,7 +275,13 @@ export class IMRenderer {
275
275
  name,
276
276
  ok,
277
277
  ...(result !== undefined && { result }),
278
- ...(error !== undefined && { error }),
278
+ ...(!ok
279
+ ? { error: getToolResultErrorText({ name, result, error, exitCode: classification?.exitCode }) }
280
+ : error !== undefined ? { error } : {}),
281
+ ...(typeof classification?.exitCode === 'number' && Number.isInteger(classification.exitCode)
282
+ ? { exit_code: classification.exitCode }
283
+ : {}),
284
+ ...(classification?.outputKind ? { output_kind: classification.outputKind } : {}),
279
285
  ...(!ok && {
280
286
  error_code: classifyAunToolErrorCode({
281
287
  ...(classification ?? {}),
@@ -904,7 +910,13 @@ export class IMRenderer {
904
910
  item.arguments = toolInputForDisplay(event.name, event.input);
905
911
  return item;
906
912
  }
907
- case 'tool_result':
913
+ case 'tool_result': {
914
+ const executionFields = {
915
+ ...(typeof event.exitCode === 'number' && Number.isInteger(event.exitCode)
916
+ ? { exit_code: event.exitCode }
917
+ : {}),
918
+ ...(event.outputKind ? { output_kind: event.outputKind } : {}),
919
+ };
908
920
  if (event.isError) {
909
921
  const classification = normalizeToolErrorClassification(event);
910
922
  return {
@@ -912,9 +924,11 @@ export class IMRenderer {
912
924
  call_id: event.callId || this.synthCallId(),
913
925
  name: event.name,
914
926
  ok: false,
927
+ ...executionFields,
928
+ ...(event.result !== undefined ? { result: event.result } : {}),
915
929
  error_code: classifyAunToolErrorCode({ ...event, ...classification }),
916
930
  ...(classification.errorDetailCode ? { detail_code: classification.errorDetailCode } : {}),
917
- error: event.error || (typeof event.result === 'string' ? event.result : '执行失败'),
931
+ error: getToolResultErrorText(event),
918
932
  ...(event.durationMs !== undefined && { duration_ms: event.durationMs }),
919
933
  };
920
934
  }
@@ -925,11 +939,13 @@ export class IMRenderer {
925
939
  call_id: event.callId || this.synthCallId(),
926
940
  name: event.name,
927
941
  ok: true,
942
+ ...executionFields,
928
943
  result: event.result,
929
944
  text: resultText,
930
945
  ...(event.durationMs !== undefined && { duration_ms: event.durationMs }),
931
946
  };
932
947
  }
948
+ }
933
949
  case 'compact':
934
950
  return {
935
951
  kind: 'notice',