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
@@ -5,7 +5,7 @@ import { logger } from '../../utils/logger.js';
5
5
  import { encodePath } from '../../utils/cross-platform.js';
6
6
  import { readRunnerEventSummary } from './runner-event-summary.js';
7
7
  import { isInternalInjectedUserMessage, isToolResultMessage } from './session-transcript.js';
8
- import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
8
+ import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, compactJsonlTail, SESSION_META_COMPACT_POLICY, } from './session-fs-store.js';
9
9
  import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
10
10
  import { tryParseChannelKey } from '../channel-loader.js';
11
11
  import { isSystemControlChannel } from '../system-channels.js';
@@ -261,6 +261,9 @@ export class SessionManager {
261
261
  identityResolver;
262
262
  fileAdapters = new Map();
263
263
  sessionEncryptState = new Map();
264
+ /** findSessionFileById 的查找缓存:命中前一律 stat 校验,不作为真相源。 */
265
+ sessionFileIndex = new Map();
266
+ knownChatDirs = null;
264
267
  constructor(sessionsDir, eventBus, identityResolver) {
265
268
  ensureDir(sessionsDir);
266
269
  this.sessionsDir = sessionsDir;
@@ -627,6 +630,13 @@ export class SessionManager {
627
630
  appendJsonlDurable(metaPath, file);
628
631
  else
629
632
  appendJsonl(metaPath, file);
633
+ // 本次快照已落盘;压缩只回收历史行,失败不影响写入结果,原文件保持不变。
634
+ try {
635
+ compactJsonlTail(metaPath, SESSION_META_COMPACT_POLICY);
636
+ }
637
+ catch (error) {
638
+ logger.warn(`[SessionManager] meta compaction skipped for ${metaPath}: ${error instanceof Error ? error.message : String(error)}`);
639
+ }
630
640
  }
631
641
  /**
632
642
  * Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
@@ -817,24 +827,84 @@ export class SessionManager {
817
827
  }
818
828
  return results;
819
829
  }
830
+ /**
831
+ * 按 sessionId 定位 meta 文件。逐个 id 全量 scanChatDirs 在几百个 chat 目录下
832
+ * 单次约 50ms,启动时批量查询(SessionIdleMonitor 恢复 170+ 会话)会同步阻塞数秒,
833
+ * 所以首次查询时按 readdir 建一份 id→路径索引,之后:
834
+ * - 命中:stat 校验仍在才返回(文件被移进 _trash/目录被迁移时自然失效);
835
+ * - 未命中:先在已知目录里按原规则 stat 探测(覆盖索引之后新建的会话),
836
+ * 再重扫目录列表、只探测新出现的目录。不存在的 id 开销与原实现相当。
837
+ * 目录顺序与「同目录先主文件后 _threads」的优先级保持原实现语义。
838
+ */
820
839
  findSessionFileById(sessionId) {
821
- const chatDirs = scanChatDirs(this.sessionsDir);
822
- for (const { dirPath } of chatDirs) {
840
+ const cached = this.sessionFileIndex.get(sessionId);
841
+ if (cached) {
842
+ try {
843
+ fs.statSync(cached.metaPath);
844
+ return cached;
845
+ }
846
+ catch {
847
+ this.sessionFileIndex.delete(sessionId);
848
+ }
849
+ }
850
+ if (!this.knownChatDirs) {
851
+ this.rebuildSessionFileIndex();
852
+ const indexed = this.sessionFileIndex.get(sessionId);
853
+ if (indexed)
854
+ return indexed;
855
+ }
856
+ const probed = this.probeSessionFile(sessionId, this.knownChatDirs);
857
+ if (probed)
858
+ return probed;
859
+ const previous = new Set(this.knownChatDirs);
860
+ this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
861
+ return this.probeSessionFile(sessionId, this.knownChatDirs.filter(dir => !previous.has(dir)));
862
+ }
863
+ probeSessionFile(sessionId, chatDirs) {
864
+ for (const dirPath of chatDirs) {
823
865
  const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
824
866
  try {
825
867
  fs.statSync(mainPath);
826
- return { chatDir: dirPath, metaPath: mainPath, isThread: false };
868
+ return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: mainPath, isThread: false });
827
869
  }
828
870
  catch { }
829
871
  const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
830
872
  try {
831
873
  fs.statSync(threadPath);
832
- return { chatDir: dirPath, metaPath: threadPath, isThread: true };
874
+ return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: threadPath, isThread: true });
833
875
  }
834
876
  catch { }
835
877
  }
836
878
  return undefined;
837
879
  }
880
+ rememberSessionFile(sessionId, found) {
881
+ this.sessionFileIndex.set(sessionId, found);
882
+ return found;
883
+ }
884
+ rebuildSessionFileIndex() {
885
+ this.sessionFileIndex.clear();
886
+ this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
887
+ for (const dirPath of this.knownChatDirs) {
888
+ for (const [dir, isThread] of [[dirPath, false], [path.join(dirPath, '_threads'), true]]) {
889
+ let names;
890
+ try {
891
+ names = fs.readdirSync(dir);
892
+ }
893
+ catch {
894
+ continue;
895
+ }
896
+ for (const name of names) {
897
+ if (!name.endsWith('.jsonl'))
898
+ continue;
899
+ const id = name.slice(0, -'.jsonl'.length);
900
+ // 先到先得:与逐目录探测时的优先级一致。
901
+ if (!this.sessionFileIndex.has(id)) {
902
+ this.sessionFileIndex.set(id, { chatDir: dirPath, metaPath: path.join(dir, name), isThread });
903
+ }
904
+ }
905
+ }
906
+ }
907
+ }
838
908
  // ─── Public API ───
839
909
  getKnownThreadIds(channel) {
840
910
  const chatDirs = scanChatDirs(this.sessionsDir);
@@ -3,6 +3,8 @@ import path from 'path';
3
3
  import { parseDocument } from 'yaml';
4
4
  import { resolveSkills } from './switch-resolver.js';
5
5
  import { logger } from '../../utils/logger.js';
6
+ import { isDevMode } from '../../paths.js';
7
+ import { isShippedPath, readVerifiedText } from '../file-integrity/read.js';
6
8
  /**
7
9
  * 生成索引文件(汇总 → 分组 → 落两文件)。
8
10
  *
@@ -68,11 +70,22 @@ function generateOneIndex(indexDir, filename, skills, label) {
68
70
  *
69
71
  * 直接按目录读,不走 discoverSkillDirectories——后者按父目录扫描再按 name 匹配,
70
72
  * 同名技能(本机覆盖内置)会读到另一份的 description。
73
+ *
74
+ * 内置技能是随包发布的内容:安装模式下按台账校验,带外改动会被恢复成登记版本,
75
+ * 恢复不了就不进索引(返回空描述),与 ECK 文档同一套策略。开发模式下不校验,
76
+ * agent 随手改技能可以立刻生效。
77
+ *
78
+ * 注意:这里保护的是索引里那行描述。模型拿到索引后是用 Read 工具读 SKILL.md 正文的,
79
+ * 那条路径不经过本函数,目前不受台账保护(等单独的 SKILL.md 加载工具接手)。
71
80
  */
72
81
  export function readSkillDescription(skillDir) {
82
+ const file = path.join(skillDir, 'SKILL.md');
83
+ const verify = isShippedPath(file) && !isDevMode();
73
84
  try {
74
- const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
75
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
85
+ const checked = readVerifiedText(file, { verify, tag: 'skill' });
86
+ if (!checked)
87
+ return '';
88
+ const match = checked.text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
76
89
  if (!match)
77
90
  return '';
78
91
  const doc = parseDocument(match[1]);
@@ -81,7 +94,8 @@ export function readSkillDescription(skillDir) {
81
94
  const meta = doc.toJS();
82
95
  return typeof meta?.description === 'string' ? meta.description.trim() : '';
83
96
  }
84
- catch {
97
+ catch (err) {
98
+ logger.warn(`[skill] read description failed path=${file} ${err instanceof Error ? err.message : String(err)}`);
85
99
  return '';
86
100
  }
87
101
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * system-session/grants.ts — 写入目标与模块声明的可写文件逐个精确比对
3
+ *
4
+ * 输入只有 canonicalToolCall 给出的写入目标和 grants().writeFiles,不读任何 runner 的工具入参。
5
+ */
6
+ import fs from 'fs';
7
+ import { isSameOrDescendant, resolveProtectedCandidateWithoutFinalSymlink, } from '../permission/protected-paths.js';
8
+ /**
9
+ * 每个写入目标都必须正好是某个声明的可写文件:
10
+ * - 祖先目录按 realpath 解析、最后一段按字面比较(目标若是符号链接,比较的是链接本身,不跟随);
11
+ * - 目标已存在时必须是普通文件且只有一个硬链接,链接、目录、硬链接一律拒绝;
12
+ * - 相对路径按会话 cwd 解析。目标列表为空表示取不到路径,按拒绝处理。
13
+ */
14
+ export function checkWriteTargets(targets, writeFiles, cwd) {
15
+ if (targets.length === 0)
16
+ return { ok: false, reason: 'no-target' };
17
+ const allowed = writeFiles.map(file => resolveProtectedCandidateWithoutFinalSymlink(file));
18
+ for (const target of targets) {
19
+ const resolved = resolveProtectedCandidateWithoutFinalSymlink(target, cwd);
20
+ if (!allowed.some(file => samePath(resolved, file))) {
21
+ return { ok: false, reason: 'outside-grant', path: resolved };
22
+ }
23
+ let stat;
24
+ try {
25
+ stat = fs.lstatSync(resolved);
26
+ }
27
+ catch (error) {
28
+ // 尚未创建是正常情况;其余错误(权限、父路径是文件等)一律按拒绝处理。
29
+ if (error.code === 'ENOENT')
30
+ continue;
31
+ return { ok: false, reason: 'not-regular-file', path: resolved };
32
+ }
33
+ if (!stat.isFile() || stat.nlink > 1)
34
+ return { ok: false, reason: 'not-regular-file', path: resolved };
35
+ }
36
+ return { ok: true };
37
+ }
38
+ function samePath(a, b) {
39
+ return isSameOrDescendant(a, b) && isSameOrDescendant(b, a);
40
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * system-session/memory.ts — 记忆整理会话模块
3
+ *
4
+ * 写入只有两条路:记忆走 `ec memory create|update|archive`;整理反馈写 B/scratch/feedback.txt,
5
+ * 这是本会话唯一的可写文件(见 memoryGrants)。其余路径全部只读:
6
+ *
7
+ * B = 批次目录 R = relations/<peer>/memory A = personal/memory
8
+ * O = memory/owner/<owner> M = 本对话 messages.jsonl(单文件)
9
+ *
10
+ * notes B, R/processing, R/consolidated, R/MEMORY.md, M
11
+ * generalize B, A/consolidated, A/MEMORY.md, R/consolidated, M
12
+ * promote B, O/consolidated, O/MEMORY.md, R/consolidated, M
13
+ * profile B, R/consolidated, R/MEMORY.md, M
14
+ * forget B, X/consolidated, X/MEMORY.md(X=触发层级),X=relation 时加 M
15
+ *
16
+ * 详见 docs/memory/system-session-plugin-design.md §5-§8
17
+ */
18
+ import path from 'path';
19
+ import { resolveRoot } from '../../paths.js';
20
+ import { parseCodexToolCommand } from '../permission/ec-command-parser.js';
21
+ import { canonicalToolCall } from '../permission/tool-canonical.js';
22
+ import { checkReadonly } from '../permission/tool-policy.js';
23
+ import { CONSOLIDATION_MODES } from '../memory/consolidation-context.js';
24
+ import { feedbackFilePath } from '../memory/consolidation-ops-records.js';
25
+ import { checkWriteTargets } from './grants.js';
26
+ export const MEMORY_SESSION_DENY_MESSAGE = '整理会话不允许执行此命令';
27
+ const MEMORY_MANIFEST = 'eck_manifest.memory.json';
28
+ const ALLOWED_MEMORY_SUBCOMMANDS = new Set(['create', 'update', 'archive']);
29
+ const BASH_COMMAND_RULE = 'Bash 只允许单条 `ec memory create|update|archive`,或落在本次整理目录内的只读查询'
30
+ + '(不能拼接其他命令;正文含特殊字符时用单引号包住)。读文件请用 Read / Glob / Grep;'
31
+ + '整理反馈用文件写入工具写到任务书指定的反馈文件。';
32
+ function layerDir(layer, ctx) {
33
+ const root = resolveRoot();
34
+ if (layer === 'agent')
35
+ return path.join(root, 'agents', ctx.selfAid, 'personal', 'memory');
36
+ if (layer === 'relation') {
37
+ return ctx.peerKey ? path.join(root, 'agents', ctx.selfAid, 'relations', ctx.peerKey, 'memory') : undefined;
38
+ }
39
+ return ctx.ownerAid ? path.join(root, 'memory', 'owner', ctx.ownerAid) : undefined;
40
+ }
41
+ /** 本模式的可读根。缺少输入事实(如无 peerKey)的条目直接不给,fail-closed。 */
42
+ export function memoryReadRoots(ctx) {
43
+ const roots = [];
44
+ const add = (...values) => {
45
+ for (const value of values)
46
+ if (value && path.isAbsolute(value))
47
+ roots.push(value);
48
+ };
49
+ const consolidated = (layer) => {
50
+ const dir = layerDir(layer, ctx);
51
+ return dir ? [path.join(dir, 'consolidated'), path.join(dir, 'MEMORY.md')] : [];
52
+ };
53
+ const relation = layerDir('relation', ctx);
54
+ const messages = ctx.chatDir ? path.join(ctx.chatDir, 'messages.jsonl') : undefined;
55
+ // 批次目录已覆盖反馈文件,不必单独列出
56
+ add(ctx.batchDir);
57
+ switch (ctx.subtype) {
58
+ case 'notes':
59
+ add(relation && path.join(relation, 'processing'), ...consolidated('relation'), messages);
60
+ break;
61
+ case 'generalize':
62
+ add(...consolidated('agent'), relation && path.join(relation, 'consolidated'), messages);
63
+ break;
64
+ case 'promote':
65
+ add(...consolidated('owner'), relation && path.join(relation, 'consolidated'), messages);
66
+ break;
67
+ case 'profile':
68
+ add(...consolidated('relation'), messages);
69
+ break;
70
+ case 'forget':
71
+ if (ctx.memoryScope) {
72
+ add(...consolidated(ctx.memoryScope));
73
+ if (ctx.memoryScope === 'relation')
74
+ add(messages);
75
+ }
76
+ break;
77
+ default:
78
+ break;
79
+ }
80
+ return [...new Set(roots)];
81
+ }
82
+ /** 文件权限声明:读按路径矩阵,写只有本批次的反馈文件一个。缺批次目录时不给任何可写文件。 */
83
+ export function memoryGrants(ctx) {
84
+ return {
85
+ readRoots: memoryReadRoots(ctx),
86
+ writeFiles: ctx.batchDir && path.isAbsolute(ctx.batchDir) ? [feedbackFilePath(ctx.batchDir)] : [],
87
+ };
88
+ }
89
+ function deny(code, reason) {
90
+ return { allow: false, code, reason: `${MEMORY_SESSION_DENY_MESSAGE}:${reason}` };
91
+ }
92
+ /**
93
+ * 把 shell 解析失败翻译成可照做的补救动作。
94
+ *
95
+ * `ec memory create … "……"` 因正文含反引号被拒时,泛化文案会把病因指向"拼接了别的命令",
96
+ * 真正的病因是正文的引号选错了,所以按 issue 精确归因。
97
+ */
98
+ function shellIssueRemedy(issue) {
99
+ switch (issue) {
100
+ case 'unsafe-expansion':
101
+ return '自由文本参数(--desc、正文、--keywords)里的反引号或 `$` 出现在双引号内,'
102
+ + '会被 shell 当作命令替换或变量展开,所以无法确认它实际会执行什么。'
103
+ + '请把这些参数改用单引号包住(如 --desc \'……\')。'
104
+ + '注意单引号内部不能再出现单引号本身,正文可以跨多行。';
105
+ case 'invalid-quote':
106
+ return '单引号包裹的正文里出现了单引号本身,导致引号提前闭合、命令无法解析。'
107
+ + '正文里不要写半角撇号(ASCII 0x27):改用 `ʼ`(U+02BC)或 `'`(U+FF07)代替,'
108
+ + '或换一种说法避开这个字符。不要尝试转义——'
109
+ + '单引号内无法转义单引号,转义与拼接一律被拒。';
110
+ case 'structural-newline':
111
+ return '命令里存在未闭合的换行结构。把正文放进单引号内即可直接换行。';
112
+ case 'shell-composition':
113
+ return '命令里含有 shell 拼接、管道或重定向(`;` `&&` `|` `$()` 等)。整理会话只允许单条 ec memory 命令。';
114
+ case 'unsupported-escape':
115
+ return '命令里使用了不受支持的转义写法。请改用单引号包住正文,避免转义。';
116
+ case 'lossy-cmd-shim':
117
+ return '命令经过 Windows cmd 包装后无法无歧义还原。请把正文改成不含换行的单条命令。';
118
+ case 'untrusted-shell':
119
+ return '命令带有不受信任的 shell 包装。请直接写 `ec memory …`,不要经由其他 shell 转发。';
120
+ default:
121
+ return undefined;
122
+ }
123
+ }
124
+ function looksLikeMemoryCommand(command) {
125
+ return /^\s*ec(?:\.cmd|\.exe|\.ps1)?\s+memory\s+\S/.test(command);
126
+ }
127
+ /** 只读路径判定:复用 checkReadonly,可读根只来自 memoryGrants。 */
128
+ function checkReadWithinMatrix(toolName, input, ctx) {
129
+ return checkReadonly(toolName, input, ctx.projectPath, {
130
+ sessionId: ctx.sessionId,
131
+ selfAid: ctx.selfAid,
132
+ managedTempDir: ctx.managedTempDir,
133
+ grantedReadRoots: memoryGrants(ctx).readRoots,
134
+ excludeWorkspace: true,
135
+ allowProtectedMetadata: false,
136
+ });
137
+ }
138
+ /** 写入判定只看 canonicalToolCall 给出的目标,不读工具入参。 */
139
+ function checkWrite(call, ctx) {
140
+ // shell 里的 apply_patch 跑的是 runner 自带的补丁器还是任意可执行文件,取决于 runner;
141
+ // 需要它的 runner 由适配层先转成 FileChange 再交给模块,模块这里一律不认。
142
+ if (call.via === 'shell') {
143
+ return deny('memory_session_shell_write_forbidden', '不能经由 shell 写文件,整理反馈请用文件写入工具写到任务书指定的反馈文件');
144
+ }
145
+ const { writeFiles } = memoryGrants(ctx);
146
+ if (writeFiles.length === 0) {
147
+ return deny('memory_session_write_no_target', '当前会话缺少批次目录,没有可写的反馈文件');
148
+ }
149
+ const result = checkWriteTargets(call.paths, writeFiles, ctx.projectPath);
150
+ if (result.ok)
151
+ return { allow: true };
152
+ const reason = result.reason === 'no-target'
153
+ ? '无法确定写入目标'
154
+ : result.reason === 'not-regular-file'
155
+ ? `写入目标不是普通文件(链接、目录或硬链接):${result.path}`
156
+ : `只能写反馈文件 ${writeFiles[0]},不能写 ${result.path}`;
157
+ return deny('memory_session_write_outside_grant', reason);
158
+ }
159
+ function isAllowedMemoryCommand(argv) {
160
+ return argv[0] === 'ec' && argv[1] === 'memory' && ALLOWED_MEMORY_SUBCOMMANDS.has(argv[2] ?? '');
161
+ }
162
+ function checkShell(input, ctx) {
163
+ const parsed = parseCodexToolCommand(input);
164
+ if (parsed.ok && isAllowedMemoryCommand(parsed.argv))
165
+ return { allow: true, ecCommand: true };
166
+ // Codex 没有 Read 工具,读任务书只能靠可证明只读的 shell 查询
167
+ const readonly = checkReadWithinMatrix('Bash', input, ctx);
168
+ if (readonly.behavior === 'allow')
169
+ return { allow: true };
170
+ if (!parsed.ok) {
171
+ const rawCommand = typeof input.command === 'string' ? input.command : '';
172
+ const remedy = shellIssueRemedy(parsed.issue);
173
+ if (looksLikeMemoryCommand(rawCommand) && remedy) {
174
+ return deny('memory_session_command_forbidden', `这条命令本身就是 \`ec memory\`,但未能解析成单条字面命令。${remedy}`);
175
+ }
176
+ }
177
+ return deny('memory_session_command_forbidden', BASH_COMMAND_RULE);
178
+ }
179
+ function checkTool(rawToolName, input, ctx) {
180
+ const call = canonicalToolCall(rawToolName, input);
181
+ switch (call.access) {
182
+ case 'read': {
183
+ const result = checkReadWithinMatrix(call.name, input, ctx);
184
+ return result.behavior === 'allow'
185
+ ? { allow: true }
186
+ : deny(result.policyCode ?? 'memory_session_read_outside_matrix', '只能读取本次整理的批次目录、对应记忆目录与本对话流水');
187
+ }
188
+ case 'write':
189
+ // 引号内恰好出现 apply_patch 字样的 ec memory 命令可能被保守识别成 shell 写入,先按 ec 命令认定
190
+ if (call.via === 'shell') {
191
+ const parsed = parseCodexToolCommand(input);
192
+ if (parsed.ok && isAllowedMemoryCommand(parsed.argv))
193
+ return { allow: true, ecCommand: true };
194
+ }
195
+ return checkWrite(call, ctx);
196
+ case 'shell':
197
+ return checkShell(input, ctx);
198
+ default:
199
+ return deny('memory_session_tool_forbidden', `整理会话不能使用 ${rawToolName};写记忆用 \`ec memory create|update|archive\`,整理反馈写到任务书指定的反馈文件`);
200
+ }
201
+ }
202
+ function checkCommand(argv) {
203
+ if (argv[0] === 'memory' && ALLOWED_MEMORY_SUBCOMMANDS.has(argv[1] ?? ''))
204
+ return { allow: true };
205
+ return deny('memory_session_command_forbidden', '只允许 `ec memory create|update|archive`');
206
+ }
207
+ export const memorySystemSession = {
208
+ type: 'memory',
209
+ subtypes: CONSOLIDATION_MODES,
210
+ supportedRunners: ['claude', 'codex', 'ecagent'],
211
+ manifest: () => MEMORY_MANIFEST,
212
+ // 不启用 runner 自带的 OS 沙箱:整理会话要读写的路径在 $EVOLCORE_HOME 下,
213
+ // 与会话 cwd 互为兄弟目录,沙箱的「项目目录」模型套不上。边界改由 grants 单独管。
214
+ runnerLaunch: (_runner, ctx) => ({
215
+ sandbox: 'off',
216
+ runnerMode: 'readonly',
217
+ grants: memoryGrants(ctx),
218
+ }),
219
+ grants: memoryGrants,
220
+ checkTool,
221
+ checkCommand: argv => checkCommand(argv),
222
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * system-session/registry.ts — 系统会话模块注册表
3
+ *
4
+ * 只登记仓库内的模块,不做运行时动态加载、不读配置。
5
+ */
6
+ import { SYSTEM_SESSION_TYPES } from './types.js';
7
+ import { memorySystemSession } from './memory.js';
8
+ const MODULES = new Map([
9
+ [memorySystemSession.type, memorySystemSession],
10
+ ]);
11
+ if (SYSTEM_SESSION_TYPES.length !== MODULES.size || SYSTEM_SESSION_TYPES.some(type => !MODULES.has(type))) {
12
+ throw new Error('SYSTEM_SESSION_TYPES 与系统会话注册表不一致');
13
+ }
14
+ export function getSystemSessionModule(type) {
15
+ return MODULES.get(type);
16
+ }
17
+ export function isSystemSessionType(type) {
18
+ return !!type && MODULES.has(type);
19
+ }
20
+ /** 从 session.metadata 读出系统会话标记;存在但不合法时拒绝降级为普通会话。 */
21
+ export function readSystemSessionMarker(metadata) {
22
+ const rawValue = metadata?.systemSession;
23
+ if (rawValue === undefined)
24
+ return undefined;
25
+ if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
26
+ throw new Error('系统会话标记格式非法');
27
+ }
28
+ const raw = rawValue;
29
+ if (typeof raw.type !== 'string' || typeof raw.subtype !== 'string') {
30
+ throw new Error('系统会话标记缺少 type 或 subtype');
31
+ }
32
+ const module = MODULES.get(raw.type);
33
+ if (!module)
34
+ throw new Error(`未注册的系统会话类型: ${raw.type}`);
35
+ if (!module.subtypes.includes(raw.subtype)) {
36
+ throw new Error(`系统会话 ${raw.type} 不支持 subtype: ${raw.subtype}`);
37
+ }
38
+ return { type: raw.type, subtype: raw.subtype };
39
+ }
40
+ /**
41
+ * 拉起前校验:类型与 subtype 已注册、runner 受支持。
42
+ * 不合法直接抛错,调用方不应降级成普通会话。
43
+ */
44
+ export function assertSystemSessionLaunchable(marker, runner) {
45
+ const module = MODULES.get(marker.type);
46
+ if (!module)
47
+ throw new Error(`未注册的系统会话类型: ${marker.type}`);
48
+ if (!module.subtypes.includes(marker.subtype)) {
49
+ throw new Error(`系统会话 ${marker.type} 不支持 subtype: ${marker.subtype}`);
50
+ }
51
+ if (!module.supportedRunners.includes(runner)) {
52
+ throw new Error(`系统会话 ${marker.type} 不支持在 ${runner} 上运行(支持:${module.supportedRunners.join(', ')})`);
53
+ }
54
+ return module;
55
+ }
56
+ /** 从 session.metadata 构造模块输入事实。 */
57
+ export function buildSystemSessionContext(params) {
58
+ const m = params.metadata ?? {};
59
+ const str = (key) => (typeof m[key] === 'string' && m[key] ? m[key] : undefined);
60
+ const scope = str('memoryScope');
61
+ const batchDir = str('consolidationBatchDir');
62
+ return {
63
+ sessionId: params.sessionId,
64
+ subtype: params.marker.subtype,
65
+ selfAid: params.selfAid,
66
+ projectPath: params.projectPath,
67
+ ...(params.managedTempDir ? { managedTempDir: params.managedTempDir } : {}),
68
+ ...(str('peerKey') ? { peerKey: str('peerKey') } : {}),
69
+ ...(str('consolidationOwnerAid') ? { ownerAid: str('consolidationOwnerAid') } : {}),
70
+ ...(batchDir ? { batchDir } : {}),
71
+ ...(str('consolidationChatDir') ? { chatDir: str('consolidationChatDir') } : {}),
72
+ ...(scope === 'relation' || scope === 'agent' || scope === 'owner' ? { memoryScope: scope } : {}),
73
+ };
74
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * system-session/types.ts — 系统会话模块接口
3
+ *
4
+ * 系统会话(记忆整理等)与角色体系无关:不带角色、不走角色权限判定。
5
+ * 路径、工具、ec 命令、manifest、runner 启动参数全部由对应类型的代码模块决定,
6
+ * 模块的结论就是最终结论,不可配置。
7
+ *
8
+ * 详见 docs/memory/system-session-plugin-design.md
9
+ */
10
+ /**
11
+ * 已注册的系统会话类型。放在无依赖的 types.ts,供 eck / config 层判断而不引入模块实现;
12
+ * registry.ts 启动时校验与注册表一致。
13
+ */
14
+ export const SYSTEM_SESSION_TYPES = ['memory'];
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { agentSessionsDir, agentDir, agentThreadIndexPath, agentThreadSummariesDir, agentThreadSummaryPath } from '../../paths.js';
5
5
  import { atomicWriteJson, atomicReadJson } from '../../utils/atomic-write.js';
6
- import { appendJsonl, readAllJsonlLines, readLastJsonlLineStrict, chatDirPath, tryParseChannelSessionKey } from '../session/session-fs-store.js';
6
+ import { appendJsonl, appendJsonlDurable, readAllJsonlLines, readLastJsonlLine, readLastJsonlLineStrict, chatDirPath, tryParseChannelSessionKey } from '../session/session-fs-store.js';
7
7
  import { sanitizeSessionTitle } from '../session/session-title.js';
8
8
  import { ThreadContextStorage } from './thread-context-storage.js';
9
9
  const THREAD_ID_RE = /^thread-[0-9]+-[0-9a-f]{4}-[0-9a-f]{4}$/;
@@ -631,33 +631,26 @@ export class ThreadService {
631
631
  /**
632
632
  * 把话题的当前元数据追加一行到 _threads/<sessionId>.jsonl,
633
633
  * 使 rebuildIndex 读最后一行时能拿到 rename/label/status 后的最新值。
634
- * 追加的行包含 fileToSession 所需的所有字段(从第一行继承),
634
+ * 追加的行以最后一行(当前权威快照)为底,只覆盖话题字段,
635
635
  * 确保 readMetaLatest 读最后一行时不报 "baseagent is empty"。
636
+ *
637
+ * 不能从第一行继承:第一行是创建记录,agentSessionId 为 null、没有 turnState,
638
+ * 继承它会把已绑定的后端会话冲掉;而且 meta 文件超限压缩后第一行也不再是创建记录。
636
639
  */
637
640
  appendThreadSnapshot(selfAid, entry) {
638
641
  const chatDir = this.chatDirFor(selfAid, entry.peer, entry.channelType);
639
642
  const metaPath = path.join(chatDir, '_threads', `${entry.sessionId}.jsonl`);
640
643
  if (!fs.existsSync(metaPath))
641
644
  return;
642
- // Read the first line to inherit required session fields (id, baseagent, channel, etc.)
643
- // so that fileToSession never sees a line missing mandatory fields.
644
- const allLines = readAllJsonlLines(metaPath);
645
- const firstLine = allLines[0] ?? {};
645
+ const latest = readLastJsonlLine(metaPath) ?? {};
646
646
  const now = entry.lastActiveAt;
647
- appendJsonl(metaPath, {
648
- // required session fields — inherited from the original creation record
649
- id: firstLine.id ?? entry.sessionId,
650
- channel: firstLine.channel,
651
- channelType: firstLine.channelType,
652
- channelId: firstLine.channelId,
653
- sessionKey: firstLine.sessionKey,
654
- selfAID: firstLine.selfAID,
655
- baseagent: firstLine.baseagent,
656
- projectPath: firstLine.projectPath ?? '',
657
- agentSessionId: firstLine.agentSessionId,
647
+ appendJsonlDurable(metaPath, {
648
+ // session fields (id, baseagent, agentSessionId, metadata, activeTask...) — from the latest snapshot
649
+ ...latest,
650
+ id: latest.id ?? entry.sessionId,
651
+ projectPath: latest.projectPath ?? '',
658
652
  chatType: entry.peerKind === 'group' ? 'group' : 'private',
659
- metadata: firstLine.metadata ?? {},
660
- activeTask: null,
653
+ metadata: latest.metadata ?? {},
661
654
  // topic metadata — updated values
662
655
  threadId: entry.threadId,
663
656
  channelIdAlias: entry.peer,
@@ -665,7 +658,7 @@ export class ThreadService {
665
658
  label: entry.label,
666
659
  topicStatus: entry.topicStatus,
667
660
  createdAt: entry.createdAt,
668
- createdAtStr: firstLine.createdAtStr ?? '',
661
+ createdAtStr: latest.createdAtStr ?? '',
669
662
  updatedAt: now,
670
663
  updatedAtStr: new Date(now).toISOString(),
671
664
  });
@@ -6,6 +6,7 @@ import { logger } from '../utils/logger.js';
6
6
  import { clearRoleStoreCache } from '../config/role-store.js';
7
7
  import { resolveAgentLifecycle } from '../config/lifecycle.js';
8
8
  import { loadManifest, loadManifestMeta, invalidateManifestCache, evaluateWhen, renderTemplate, renderLoopSection, resolvePathWithDiag, loadSectionFiles, loadChildTemplate, buildPathMappings, shortenPath, } from './manifest-engine.js';
9
+ import { SYSTEM_SESSION_TYPES } from '../core/system-session/types.js';
9
10
  export const DEFAULT_MANIFEST_FILE = 'eck_manifest.json';
10
11
  /**
11
12
  * 会话原型(sessionType)→ manifest 文件的内置映射。
@@ -16,9 +17,13 @@ export const BUILTIN_SESSION_MANIFESTS = {
16
17
  memory: 'eck_manifest.memory.json',
17
18
  auxiliary: 'eck_manifest.auxiliary.json',
18
19
  };
19
- /** 选择顺序:config 覆盖 → 内置映射 → 主 manifest。 */
20
+ /**
21
+ * 选择顺序:config 覆盖 → 内置映射 → 主 manifest。
22
+ * 系统会话类型不接受 config 覆盖:其 manifest 由 system-session 模块决定。
23
+ */
20
24
  export function resolveSessionManifest(sessionType, overrides) {
21
- return overrides?.[sessionType] ?? BUILTIN_SESSION_MANIFESTS[sessionType] ?? DEFAULT_MANIFEST_FILE;
25
+ const override = SYSTEM_SESSION_TYPES.includes(sessionType) ? undefined : overrides?.[sessionType];
26
+ return override ?? BUILTIN_SESSION_MANIFESTS[sessionType] ?? DEFAULT_MANIFEST_FILE;
22
27
  }
23
28
  // ── Caches ──
24
29
  const _sessionPathCache = new Map();