evolcore 0.0.3 → 0.0.5

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 (148) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
@@ -1,7 +1,7 @@
1
1
  import { ensureDir } from '../../utils/atomic-write.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { encodePath } from '../../utils/cross-platform.js';
4
- import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
4
+ import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
5
5
  import { sessionToFile, fileToSession } from './session-mapper.js';
6
6
  import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
7
7
  import { tryParseChannelKey } from '../channel-loader.js';
@@ -9,6 +9,22 @@ import { isSystemControlChannel } from '../system-channels.js';
9
9
  import path from 'path';
10
10
  import fs from 'fs';
11
11
  import os from 'os';
12
+ function newSessionStableMetadata(chatType, ...sources) {
13
+ const keys = chatType === 'group'
14
+ ? ['channelKey', 'groupId', 'groupName', 'dispatchMode']
15
+ : ['channelKey', 'peerId', 'peerName', 'peerType'];
16
+ const metadata = {};
17
+ for (const source of sources) {
18
+ if (!source)
19
+ continue;
20
+ for (const key of keys) {
21
+ const value = source[key];
22
+ if (value !== undefined && value !== null && value !== '')
23
+ metadata[key] = value;
24
+ }
25
+ }
26
+ return metadata;
27
+ }
12
28
  /** 判定用户是否为指定渠道的 admin */
13
29
  /** 判定用户是否为指定渠道的 member */
14
30
  export class SessionManager {
@@ -23,6 +39,7 @@ export class SessionManager {
23
39
  this.eventBus = eventBus;
24
40
  this.identityResolver = identityResolver;
25
41
  this.migrateChannelKeyFormat();
42
+ this.migrateUnknownAunDirs();
26
43
  this.migrateWecomChannelDirs();
27
44
  }
28
45
  setIdentityResolver(resolver) {
@@ -1098,18 +1115,20 @@ export class SessionManager {
1098
1115
  }
1099
1116
  }
1100
1117
  }
1101
- async createNewSession(channel, channelId, projectPath, name, baseagent) {
1118
+ async createNewSession(channel, channelId, projectPath, name, baseagent, identityMetadata) {
1102
1119
  const inheritedChatType = this.getActiveChatType(channel, channelId);
1103
1120
  // Derive selfAID and channelType from existing sessions
1104
1121
  const existingDir = this.findExistingChatDir(channel, channelId);
1105
1122
  const identity = this.deriveChannelIdentity(channel, channelId);
1106
1123
  let channelType = identity.channelType;
1107
1124
  let selfAID = identity.selfAID;
1125
+ let activeMetadata;
1108
1126
  if (existingDir) {
1109
1127
  const active = readJsonFile(path.join(existingDir, 'active.json'));
1110
1128
  if (active) {
1111
1129
  channelType = active.channelType || channel;
1112
1130
  selfAID = active.selfAID || '';
1131
+ activeMetadata = active.metadata;
1113
1132
  // 兼容旧字段名
1114
1133
  const activeBaseagent = active.baseagent || active.agentType;
1115
1134
  if (!baseagent && activeBaseagent)
@@ -1130,7 +1149,7 @@ export class SessionManager {
1130
1149
  baseagent,
1131
1150
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1132
1151
  chatType: inheritedChatType,
1133
- metadata: {},
1152
+ metadata: newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata),
1134
1153
  name: name || '默认会话',
1135
1154
  createdAt: Date.now(),
1136
1155
  updatedAt: Date.now(),
@@ -1258,7 +1277,7 @@ export class SessionManager {
1258
1277
  * 为超时后的独立话题创建新会话。保留聊天路由和稳定 metadata,
1259
1278
  * 但不继承任何底层 agent session 或临时任务状态。
1260
1279
  */
1261
- async createRenewedSession(sourceSession) {
1280
+ async createRenewedSession(sourceSession, context) {
1262
1281
  const metadata = { ...(sourceSession.metadata || {}) };
1263
1282
  delete metadata.agentSessions;
1264
1283
  delete metadata.resumeAt;
@@ -1289,7 +1308,11 @@ export class SessionManager {
1289
1308
  projectPath: session.projectPath,
1290
1309
  name: session.name,
1291
1310
  chatType: session.chatType,
1311
+ cause: 'renew',
1312
+ sourceSessionId: sourceSession.id,
1313
+ renewSource: context?.source,
1292
1314
  timestamp: Date.now(),
1315
+ causation: context?.causation,
1293
1316
  });
1294
1317
  return session;
1295
1318
  }
@@ -1640,4 +1663,209 @@ export class SessionManager {
1640
1663
  logger.info(`[SessionManager] Migrated ${migrated} WeCom session director${migrated === 1 ? 'y' : 'ies'} to channelKey isolation`);
1641
1664
  }
1642
1665
  }
1666
+ /**
1667
+ * Recover legacy AUN chats written beneath `_unknown` before selfAID was
1668
+ * persisted. A channel key or persisted selfAID is required to make the
1669
+ * migration unambiguous; all other directories remain untouched.
1670
+ */
1671
+ migrateUnknownAunDirs() {
1672
+ const legacyDirs = scanChatDirs(this.sessionsDir)
1673
+ .filter(entry => entry.channelType === 'aun' && entry.selfAID === '_unknown');
1674
+ let migrated = 0;
1675
+ for (const entry of legacyDirs) {
1676
+ const selfAIDs = this.findAunSelfAIDCandidates(entry.dirPath);
1677
+ if (selfAIDs.size !== 1) {
1678
+ if (selfAIDs.size > 1) {
1679
+ logger.error(`[SessionManager] Cannot auto-migrate mixed unknown AUN session directory: ${entry.dirPath}`);
1680
+ }
1681
+ continue;
1682
+ }
1683
+ const selfAID = [...selfAIDs][0];
1684
+ const target = chatDirPath(this.sessionsDir, 'aun', entry.channelId, selfAID);
1685
+ try {
1686
+ this.mergeUnknownAunDirectory(entry.dirPath, target, selfAID);
1687
+ migrated++;
1688
+ }
1689
+ catch (error) {
1690
+ logger.error(`[SessionManager] Failed to migrate unknown AUN session directory ${entry.dirPath}: ${String(error)}`);
1691
+ }
1692
+ }
1693
+ if (migrated > 0) {
1694
+ logger.info(`[SessionManager] Migrated ${migrated} unknown AUN session director${migrated === 1 ? 'y' : 'ies'} to selfAID isolation`);
1695
+ }
1696
+ }
1697
+ findAunSelfAIDCandidates(chatDir) {
1698
+ const candidates = new Set();
1699
+ const visitRecord = (record) => {
1700
+ if (!record || typeof record !== 'object')
1701
+ return;
1702
+ const session = record;
1703
+ const direct = typeof session.selfAID === 'string' ? session.selfAID.trim() : '';
1704
+ if (direct && direct !== '_unknown')
1705
+ candidates.add(direct);
1706
+ const channel = typeof session.channel === 'string' ? tryParseChannelKey(session.channel) : null;
1707
+ if (channel?.type === 'aun' && channel.selfAID !== '_unknown')
1708
+ candidates.add(channel.selfAID);
1709
+ };
1710
+ const visitJsonl = (filePath) => {
1711
+ for (const record of readAllJsonlLines(filePath))
1712
+ visitRecord(record);
1713
+ };
1714
+ visitRecord(readJsonFile(path.join(chatDir, 'active.json')));
1715
+ for (const metaFile of scanMetaFiles(chatDir))
1716
+ visitJsonl(path.join(chatDir, metaFile));
1717
+ const threadDir = path.join(chatDir, '_threads');
1718
+ for (const metaFile of scanMetaFiles(threadDir))
1719
+ visitJsonl(path.join(threadDir, metaFile));
1720
+ return candidates;
1721
+ }
1722
+ mergeUnknownAunDirectory(sourceDir, targetDir, selfAID) {
1723
+ if (sourceDir === targetDir)
1724
+ return;
1725
+ fs.mkdirSync(targetDir, { recursive: true });
1726
+ this.mergeUnknownAunDirectoryContents(sourceDir, targetDir, selfAID);
1727
+ fs.rmdirSync(sourceDir);
1728
+ }
1729
+ mergeUnknownAunDirectoryContents(sourceDir, targetDir, selfAID) {
1730
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
1731
+ const sourcePath = path.join(sourceDir, entry.name);
1732
+ const targetPath = path.join(targetDir, entry.name);
1733
+ if (entry.isDirectory()) {
1734
+ fs.mkdirSync(targetPath, { recursive: true });
1735
+ this.mergeUnknownAunDirectoryContents(sourcePath, targetPath, selfAID);
1736
+ fs.rmdirSync(sourcePath);
1737
+ continue;
1738
+ }
1739
+ if (!entry.isFile())
1740
+ continue;
1741
+ if (entry.name === 'active.json') {
1742
+ this.mergeUnknownAunActiveFile(sourcePath, targetPath, selfAID);
1743
+ }
1744
+ else if (entry.name.endsWith('.jsonl')) {
1745
+ this.mergeUnknownAunJsonlFile(sourcePath, targetPath, selfAID);
1746
+ }
1747
+ else if (entry.name.endsWith('.json')) {
1748
+ this.mergeUnknownAunJsonFile(sourcePath, targetPath);
1749
+ }
1750
+ else if (!fs.existsSync(targetPath)) {
1751
+ fs.renameSync(sourcePath, targetPath);
1752
+ }
1753
+ else if (fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath))) {
1754
+ fs.unlinkSync(sourcePath);
1755
+ }
1756
+ else {
1757
+ fs.renameSync(sourcePath, this.uniqueLegacyFilePath(targetPath));
1758
+ }
1759
+ }
1760
+ }
1761
+ mergeUnknownAunActiveFile(sourcePath, targetPath, selfAID) {
1762
+ const source = this.normalizeAunSessionRecord(readJsonFile(sourcePath), selfAID);
1763
+ const target = this.normalizeAunSessionRecord(readJsonFile(targetPath), selfAID);
1764
+ const sourceUpdatedAt = typeof source?.updatedAt === 'number' ? source.updatedAt : 0;
1765
+ const targetUpdatedAt = typeof target?.updatedAt === 'number' ? target.updatedAt : 0;
1766
+ const selected = target && targetUpdatedAt >= sourceUpdatedAt ? target : source;
1767
+ if (selected)
1768
+ atomicWriteJson(targetPath, selected);
1769
+ fs.unlinkSync(sourcePath);
1770
+ }
1771
+ mergeUnknownAunJsonlFile(sourcePath, targetPath, selfAID) {
1772
+ const records = [
1773
+ ...this.readNormalizedAunJsonl(sourcePath, selfAID),
1774
+ ...this.readNormalizedAunJsonl(targetPath, selfAID),
1775
+ ];
1776
+ const seen = new Set();
1777
+ const unique = records.filter(record => {
1778
+ if (seen.has(record.key))
1779
+ return false;
1780
+ seen.add(record.key);
1781
+ return true;
1782
+ });
1783
+ unique.sort((left, right) => left.timestamp - right.timestamp || left.order - right.order);
1784
+ atomicWriteText(targetPath, unique.map(record => record.line).join('\n') + (unique.length ? '\n' : ''));
1785
+ fs.unlinkSync(sourcePath);
1786
+ }
1787
+ readNormalizedAunJsonl(filePath, selfAID) {
1788
+ let content;
1789
+ try {
1790
+ content = fs.readFileSync(filePath, 'utf-8');
1791
+ }
1792
+ catch (error) {
1793
+ if (error?.code === 'ENOENT')
1794
+ return [];
1795
+ throw error;
1796
+ }
1797
+ return content.split('\n').flatMap((rawLine, order) => {
1798
+ const line = rawLine.trim();
1799
+ if (!line)
1800
+ return [];
1801
+ try {
1802
+ const record = this.normalizeAunSessionRecord(JSON.parse(line), selfAID);
1803
+ const normalizedLine = JSON.stringify(record);
1804
+ return [{
1805
+ key: this.stableJson(record),
1806
+ line: normalizedLine,
1807
+ timestamp: this.recordTimestamp(record),
1808
+ order,
1809
+ }];
1810
+ }
1811
+ catch {
1812
+ return [{ key: `raw:${line}`, line, timestamp: Number.MAX_SAFE_INTEGER, order }];
1813
+ }
1814
+ });
1815
+ }
1816
+ normalizeAunSessionRecord(record, selfAID) {
1817
+ if (!record || typeof record !== 'object' || Array.isArray(record))
1818
+ return record;
1819
+ const result = { ...record };
1820
+ const isSessionRecord = 'channel' in result || 'channelType' in result || 'sessionKey' in result || 'selfAID' in result;
1821
+ if (!isSessionRecord)
1822
+ return result;
1823
+ if (!result.selfAID || result.selfAID === '_unknown')
1824
+ result.selfAID = selfAID;
1825
+ return result;
1826
+ }
1827
+ stableJson(value) {
1828
+ if (Array.isArray(value))
1829
+ return `[${value.map(item => this.stableJson(item)).join(',')}]`;
1830
+ if (value && typeof value === 'object') {
1831
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${this.stableJson(value[key])}`).join(',')}}`;
1832
+ }
1833
+ return JSON.stringify(value);
1834
+ }
1835
+ recordTimestamp(record) {
1836
+ if (!record || typeof record !== 'object')
1837
+ return Number.MAX_SAFE_INTEGER;
1838
+ const value = record;
1839
+ for (const key of ['ts', 'at', 'updatedAt', 'createdAt']) {
1840
+ if (typeof value[key] === 'number' && Number.isFinite(value[key]))
1841
+ return value[key];
1842
+ }
1843
+ return Number.MAX_SAFE_INTEGER;
1844
+ }
1845
+ mergeUnknownAunJsonFile(sourcePath, targetPath) {
1846
+ if (!fs.existsSync(targetPath)) {
1847
+ fs.renameSync(sourcePath, targetPath);
1848
+ return;
1849
+ }
1850
+ const source = readJsonFile(sourcePath);
1851
+ const target = readJsonFile(targetPath);
1852
+ if (source && target && !Array.isArray(source) && !Array.isArray(target)) {
1853
+ atomicWriteJson(targetPath, { ...source, ...target });
1854
+ fs.unlinkSync(sourcePath);
1855
+ return;
1856
+ }
1857
+ if (fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath))) {
1858
+ fs.unlinkSync(sourcePath);
1859
+ return;
1860
+ }
1861
+ fs.renameSync(sourcePath, this.uniqueLegacyFilePath(targetPath));
1862
+ }
1863
+ uniqueLegacyFilePath(targetPath) {
1864
+ let index = 1;
1865
+ let candidate = `${targetPath}.legacy-unknown`;
1866
+ while (fs.existsSync(candidate)) {
1867
+ candidate = `${targetPath}.legacy-unknown-${index++}`;
1868
+ }
1869
+ return candidate;
1870
+ }
1643
1871
  }
@@ -3,6 +3,8 @@ import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
3
3
  export function sessionToFile(session) {
4
4
  const metadata = {};
5
5
  if (session.metadata) {
6
+ if (session.metadata.channelKey)
7
+ metadata.channelKey = session.metadata.channelKey;
6
8
  if (session.metadata.peerId)
7
9
  metadata.peerId = session.metadata.peerId;
8
10
  if (session.metadata.peerName)
@@ -3,8 +3,11 @@ import { formatPeerKey } from '../relation/peer-identity.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { isTransientProtocolMessage, messageLogPath } from '../message/message-log.js';
5
5
  import { readAllJsonlLines } from './session-fs-store.js';
6
+ import { ResponseModeRegistry } from '../../response-system/registry.js';
7
+ import { registerBuiltinModes } from '../../response-system/modes/index.js';
8
+ import { resolveEffectiveResponseModeConfig } from '../../response-system/config-resolver.js';
9
+ import { normalizeBaseagent } from '../../agents/baseagent.js';
6
10
  const DEFAULT_AFTER_HOURS = 24;
7
- const DEFAULT_EFFORT = 'low';
8
11
  const DEFAULT_FALLBACK = 'continue';
9
12
  const MAX_CONTEXT_MESSAGES = 40;
10
13
  const MAX_CONTEXT_CHARS = 16_000;
@@ -12,7 +15,7 @@ const MAX_MESSAGE_CHARS = 2_000;
12
15
  const MODEL_TIMEOUT_MS = 10_000;
13
16
  const RECENT_DECISION_TTL_MS = 30_000;
14
17
  const NEW_CONFIDENCE_THRESHOLD = 0.85;
15
- const JUDGE_BASEAGENT = 'claude';
18
+ const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
16
19
  const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
17
20
  const EXPLICIT_CONTINUE_RE = /^(?:继续上次|延续上次|继续|接着)[::,,;;。!!\n]/;
18
21
  const NON_CONVERSATION_TYPES = new Set([
@@ -43,41 +46,6 @@ export function selectSessionRenewContext(entries) {
43
46
  const selected = [entries[0], ...entries.slice(-(MAX_CONTEXT_MESSAGES - 1))];
44
47
  return trimContextByChars(selected);
45
48
  }
46
- export function selectLatestHaikuModel(models) {
47
- let selected;
48
- for (const rawModel of models) {
49
- const id = rawModel.trim();
50
- if (!id)
51
- continue;
52
- const leaf = id.toLowerCase().split('/').at(-1) ?? '';
53
- const tokens = leaf.split('-');
54
- const haikuIndex = tokens.indexOf('haiku');
55
- if (haikuIndex < 0)
56
- continue;
57
- const versionTokens = haikuIndex <= 1
58
- ? tokens.slice(haikuIndex + 1)
59
- : tokens.slice(1, haikuIndex);
60
- const versions = versionTokens
61
- .filter(token => /^\d{1,2}$/.test(token))
62
- .map(Number);
63
- const dates = tokens
64
- .filter(token => /^\d{8}$/.test(token))
65
- .map(Number);
66
- const candidate = {
67
- id,
68
- major: versions[0] ?? 0,
69
- minor: versions[1] ?? 0,
70
- date: dates.length > 0 ? Math.max(...dates) : 0,
71
- };
72
- if (!selected
73
- || candidate.major > selected.major
74
- || (candidate.major === selected.major && candidate.minor > selected.minor)
75
- || (candidate.major === selected.major && candidate.minor === selected.minor && candidate.date > selected.date)) {
76
- selected = candidate;
77
- }
78
- }
79
- return selected?.id;
80
- }
81
49
  function trimContextByChars(entries) {
82
50
  if (entries.length === 0)
83
51
  return [];
@@ -100,17 +68,27 @@ export class SessionRenewService {
100
68
  locks = new Map();
101
69
  recentDecisions = new Map();
102
70
  now;
103
- configResolver;
71
+ effectiveConfigResolver;
72
+ eventBus;
73
+ responseModeRegistry = new ResponseModeRegistry();
104
74
  constructor(sessionManager, inferenceProviderResolver, options = {}) {
105
75
  this.sessionManager = sessionManager;
106
76
  this.inferenceProviderResolver = inferenceProviderResolver;
107
77
  this.now = options.now ?? (() => Date.now());
108
- this.configResolver = options.resolveConfig;
78
+ this.effectiveConfigResolver = options.resolveEffectiveConfig;
79
+ this.eventBus = options.eventBus;
80
+ registerBuiltinModes(this.responseModeRegistry);
109
81
  }
110
82
  async resolve(request) {
111
- const config = this.resolveConfig(request);
83
+ const effectiveConfig = this.resolveEffectiveConfig(request);
84
+ const config = effectiveConfig?.session_renew;
112
85
  if (!config || config.enabled !== true)
113
86
  return { session: request.session, renewed: false };
87
+ // Thread / processing sessions are already an explicit routing decision.
88
+ // Do not let a cached or active main session replace them before resolveLocked().
89
+ if (request.session.threadId || request.session.processingState) {
90
+ return { session: request.session, renewed: false };
91
+ }
114
92
  const routeKey = this.routeKey(request);
115
93
  return await this.withLock(routeKey, async () => {
116
94
  const cached = this.recentDecisions.get(routeKey);
@@ -133,12 +111,11 @@ export class SessionRenewService {
133
111
  active.identity = request.session.identity;
134
112
  return { session: active, renewed: true, decision: 'new', source: 'fallback' };
135
113
  }
136
- return await this.resolveLocked(routeKey, request, config);
114
+ return await this.resolveLocked(routeKey, request, config, effectiveConfig);
137
115
  });
138
116
  }
139
- async resolveLocked(routeKey, request, config) {
140
- if (request.session.threadId || request.session.processingState)
141
- return { session: request.session, renewed: false };
117
+ async resolveLocked(routeKey, request, config, effectiveConfig) {
118
+ const evaluationStartedAt = Date.now();
142
119
  const afterHours = validPositiveNumber(config.after_hours) ?? DEFAULT_AFTER_HOURS;
143
120
  const entries = this.readConversationEntries(request.session);
144
121
  const last = entries[entries.length - 1];
@@ -146,7 +123,7 @@ export class SessionRenewService {
146
123
  if (request.isNewSession)
147
124
  return { session: request.session, renewed: false };
148
125
  const idleMs = this.now() - request.session.updatedAt;
149
- return await this.finishDecision(routeKey, request, 'new', 'missing_history', idleMs);
126
+ return await this.finishDecision(routeKey, request, 'new', 'missing_history', idleMs, evaluationStartedAt);
150
127
  }
151
128
  const idleMs = this.now() - last.ts;
152
129
  if (idleMs <= afterHours * 60 * 60 * 1000)
@@ -162,13 +139,22 @@ export class SessionRenewService {
162
139
  source = 'reply';
163
140
  }
164
141
  else {
165
- let judgeModel;
142
+ const auxiliary = this.resolveAuxiliaryConfig(effectiveConfig, request.session.baseagent);
143
+ let judgeMetadata = {
144
+ responseMode: auxiliary.responseMode,
145
+ baseagent: request.session.baseagent,
146
+ model: auxiliary.model,
147
+ effort: auxiliary.effort,
148
+ };
166
149
  try {
167
- const provider = this.inferenceProviderResolver.getTextInferenceProvider(request.channelName, JUDGE_BASEAGENT, request.selfAid);
150
+ if (!auxiliary.model)
151
+ throw new SessionRenewJudgeError('auxiliary_model_missing');
152
+ if (auxiliary.invalidEffort)
153
+ throw new SessionRenewJudgeError('auxiliary_effort_invalid');
154
+ const provider = this.inferenceProviderResolver.getTextInferenceProvider(request.channelName, request.session.baseagent, request.selfAid);
168
155
  if (!provider)
169
- throw new Error(`text inference provider unavailable: ${JUDGE_BASEAGENT}`);
170
- const judged = await this.judgeWithModel(request, config, entries, idleMs, provider);
171
- judgeModel = judged.model;
156
+ throw new SessionRenewJudgeError('provider_unavailable');
157
+ const judged = await this.judgeWithModel(request, entries, idleMs, provider, auxiliary);
172
158
  decision = judged.decision === 'new' && judged.confidence < NEW_CONFIDENCE_THRESHOLD
173
159
  ? 'continue'
174
160
  : judged.decision;
@@ -177,15 +163,19 @@ export class SessionRenewService {
177
163
  catch (error) {
178
164
  decision = config.fallback_action ?? DEFAULT_FALLBACK;
179
165
  source = 'fallback';
166
+ judgeMetadata = { ...judgeMetadata, fallbackReason: sessionRenewFallbackReason(error) };
180
167
  logger.warn(`[SessionRenew] judge failed, using fallback=${decision}: ${error instanceof Error ? error.message : String(error)}`);
181
168
  }
182
- return await this.finishDecision(routeKey, request, decision, source, idleMs, judgeModel);
169
+ return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judgeMetadata);
183
170
  }
184
- return await this.finishDecision(routeKey, request, decision, source, idleMs);
171
+ return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt);
185
172
  }
186
- async finishDecision(routeKey, request, decision, source, idleMs, judgeModel) {
173
+ async finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judge) {
187
174
  const selected = decision === 'new'
188
- ? await this.sessionManager.createRenewedSession(request.session)
175
+ ? await this.sessionManager.createRenewedSession(request.session, {
176
+ source,
177
+ causation: request.causation,
178
+ })
189
179
  : request.session;
190
180
  selected.identity = request.session.identity;
191
181
  this.recentDecisions.set(routeKey, {
@@ -195,12 +185,39 @@ export class SessionRenewService {
195
185
  source,
196
186
  expiresAt: this.now() + RECENT_DECISION_TTL_MS,
197
187
  });
198
- logger.info(`[SessionRenew] session=${request.session.id} selected=${selected.id} decision=${decision} source=${source} idleHours=${(idleMs / 3_600_000).toFixed(2)} baseagent=${judgeModel ? JUDGE_BASEAGENT : 'none'} model=${judgeModel || 'none'}`);
188
+ logger.info(`[SessionRenew] session=${request.session.id} selected=${selected.id} decision=${decision} source=${source}`
189
+ + ` idleHours=${(idleMs / 3_600_000).toFixed(2)}`
190
+ + ` responseMode=${judge?.responseMode ?? 'none'}`
191
+ + ` baseagent=${judge?.baseagent ?? 'none'}`
192
+ + ` model=${judge?.model ?? 'none'}`
193
+ + ` effort=${judge?.effort ?? 'provider-default'}`
194
+ + ` fallbackReason=${judge?.fallbackReason ?? 'none'}`);
195
+ this.eventBus?.publish({
196
+ type: 'session:renew-evaluated',
197
+ candidateSessionId: request.session.id,
198
+ selectedSessionId: selected.id,
199
+ renewed: decision === 'new',
200
+ decision,
201
+ source,
202
+ channel: request.session.channel,
203
+ channelName: request.channelName,
204
+ channelId: request.channelId,
205
+ selfAid: request.selfAid,
206
+ idleHours: Number((idleMs / 3_600_000).toFixed(2)),
207
+ responseMode: judge?.responseMode,
208
+ baseagent: judge?.baseagent,
209
+ model: judge?.model,
210
+ effort: judge?.effort,
211
+ fallbackReason: judge?.fallbackReason,
212
+ durationMs: Math.max(0, Date.now() - evaluationStartedAt),
213
+ timestamp: this.now(),
214
+ causation: request.causation,
215
+ });
199
216
  return { session: selected, renewed: decision === 'new', decision, source };
200
217
  }
201
- resolveConfig(request) {
202
- if (this.configResolver)
203
- return this.configResolver(request);
218
+ resolveEffectiveConfig(request) {
219
+ if (this.effectiveConfigResolver)
220
+ return this.effectiveConfigResolver(request);
204
221
  if (!request.selfAid)
205
222
  return undefined;
206
223
  const peerKeyId = request.chatType === 'group'
@@ -210,7 +227,30 @@ export class SessionRenewService {
210
227
  return resolveEffective({
211
228
  self: request.selfAid,
212
229
  peerKey,
213
- }, { cache: true }).session_renew;
230
+ }, { cache: true, expand: true });
231
+ }
232
+ resolveAuxiliaryConfig(config, baseagent) {
233
+ const resolved = resolveEffectiveResponseModeConfig(this.responseModeRegistry, config);
234
+ const rawModel = resolved.modeConfig.auxiliaryModel;
235
+ const canonicalBaseagent = normalizeBaseagent(baseagent).canonical;
236
+ const selectedModel = typeof rawModel === 'string'
237
+ ? rawModel
238
+ : rawModel && typeof rawModel === 'object' && !Array.isArray(rawModel)
239
+ ? rawModel[canonicalBaseagent]
240
+ : undefined;
241
+ const model = typeof selectedModel === 'string' && selectedModel.trim()
242
+ ? selectedModel.trim()
243
+ : undefined;
244
+ const rawEffort = resolved.modeConfig.auxiliaryEffort;
245
+ const effort = typeof rawEffort === 'string' && AUXILIARY_EFFORTS.has(rawEffort)
246
+ ? rawEffort
247
+ : undefined;
248
+ return {
249
+ responseMode: resolved.mode.id,
250
+ model,
251
+ effort,
252
+ invalidEffort: rawEffort !== undefined && effort === undefined,
253
+ };
214
254
  }
215
255
  readConversationEntries(session) {
216
256
  const entries = readAllJsonlLines(messageLogPath(this.sessionManager.getChatDir(session)));
@@ -218,7 +258,7 @@ export class SessionRenewService {
218
258
  .filter(entry => isSessionRenewConversationEntry(entry, session.id))
219
259
  .sort((left, right) => left.ts - right.ts);
220
260
  }
221
- async judgeWithModel(request, config, entries, idleMs, provider) {
261
+ async judgeWithModel(request, entries, idleMs, provider, auxiliary) {
222
262
  const context = selectSessionRenewContext(entries).map(entry => ({
223
263
  dir: entry.dir,
224
264
  content: entry.content.slice(0, MAX_MESSAGE_CHARS),
@@ -243,20 +283,14 @@ export class SessionRenewService {
243
283
  let timer;
244
284
  try {
245
285
  const operation = (async () => {
246
- if (!provider.listModels)
247
- throw new Error('Claude text inference provider does not expose a model list');
248
- const models = await provider.listModels(controller.signal);
249
- const model = selectLatestHaikuModel(models);
250
- if (!model)
251
- throw new Error('Claude model list contains no Haiku model');
252
286
  const text = await provider.completeText({
253
- model,
254
- effort: config.effort || DEFAULT_EFFORT,
287
+ model: auxiliary.model,
288
+ ...(auxiliary.effort ? { effort: auxiliary.effort } : {}),
255
289
  system: systemPrompt,
256
290
  input: prompt,
257
291
  signal: controller.signal,
258
292
  });
259
- return { model, text };
293
+ return { model: auxiliary.model, text };
260
294
  })();
261
295
  const timeout = new Promise((_resolve, reject) => {
262
296
  timer = setTimeout(() => {
@@ -292,6 +326,28 @@ export class SessionRenewService {
292
326
  }
293
327
  }
294
328
  }
329
+ class SessionRenewJudgeError extends Error {
330
+ code;
331
+ constructor(code) {
332
+ super(code);
333
+ this.code = code;
334
+ this.name = 'SessionRenewJudgeError';
335
+ }
336
+ }
337
+ function sessionRenewFallbackReason(error) {
338
+ if (error instanceof SessionRenewJudgeError)
339
+ return error.code;
340
+ const message = error instanceof Error ? error.message : String(error);
341
+ if (/timed out|abort/i.test(message))
342
+ return 'timeout';
343
+ if (/HTTP \d+/i.test(message))
344
+ return 'http_error';
345
+ if (/no JSON|invalid decision|invalid confidence/i.test(message))
346
+ return 'invalid_response';
347
+ if (/no text|empty/i.test(message))
348
+ return 'empty_response';
349
+ return 'inference_error';
350
+ }
295
351
  function validPositiveNumber(value) {
296
352
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
297
353
  }
@@ -101,7 +101,11 @@ export class SessionTurnCoordinator {
101
101
  turnId: active.turnId,
102
102
  taskId: active.taskId,
103
103
  generation: active.generation,
104
- status: reason === 'stop' || reason === 'explicit_cancel' ? 'cancelled' : 'superseded',
104
+ status: reason === 'daemon_restart'
105
+ ? 'interrupted'
106
+ : reason === 'stop' || reason === 'explicit_cancel'
107
+ ? 'cancelled'
108
+ : 'superseded',
105
109
  reason,
106
110
  finishedAt: Date.now(),
107
111
  },
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { isEckSnapshotsEnabled } from '../config-store.js';
4
4
  import { eckDebugDir } from '../paths.js';
5
5
  import { logger } from '../utils/logger.js';
6
+ import { clearRoleStoreCache } from '../config/role-store.js';
6
7
  import { loadManifest, loadManifestMeta, invalidateManifestCache, evaluateWhen, renderTemplate, renderLoopSection, resolvePathWithDiag, loadSectionFiles, loadChildTemplate, buildPathMappings, shortenPath, } from './manifest-engine.js';
7
8
  const DEFAULT_MANIFEST_FILE = 'eck_manifest.json';
8
9
  // ── Caches ──
@@ -19,9 +20,18 @@ export function loadKitManifest() {
19
20
  const sections = loadManifest(DEFAULT_MANIFEST_FILE);
20
21
  logger.info(`[KitRenderer] Loaded manifest: ${sections.length} sections`);
21
22
  }
23
+ /**
24
+ * kits 统一失效入口:`kits/` 下的任何东西改了,都从这里一次清干净。
25
+ *
26
+ * 必须连带清 role-store 快照:它的签名含角色模板 mtime,而模板内容走 FileCache
27
+ * 的 on-reload(平时不查盘)。若"模板改盘 → 失效前发生一次鉴权读取",快照就会以
28
+ * **新 mtime + 旧内容** 落盘;此后签名不再变化,光清 FileCache 也换不掉它——
29
+ * 授权数据会永久停在旧版本。
30
+ */
22
31
  export function invalidateKitCache() {
23
32
  invalidateManifestCache();
24
33
  _sessionPathCache.clear();
34
+ clearRoleStoreCache();
25
35
  }
26
36
  export function invalidateSessionCache(sessionId) {
27
37
  _sessionPathCache.delete(sessionId);
@@ -203,7 +213,7 @@ const PARAM_DESCRIPTIONS = {
203
213
  sameEgressIp: '对端与本端共享同一出口 IP',
204
214
  groupId: '群组 ID(群聊时)',
205
215
  groupRulesPath: '群规则本地物化文件(relations/<peerKey>/rules.md)',
206
- groupRulesStatus: '群规则同步状态(synced/cached/missing/forbidden/invalid_metadata/file_mismatch/too_large/unreadable/error)',
216
+ groupRulesStatus: '群规则状态(ignored/synced/cached/missing/forbidden/invalid_metadata/file_mismatch/too_large/unreadable/error)',
207
217
  groupRulesError: '群规则同步错误',
208
218
  chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
209
219
  channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
@@ -226,6 +236,7 @@ const PARAM_DESCRIPTIONS = {
226
236
  osInfo: '操作系统及版本(如 Windows 11 Pro (win32 10.0.26200))',
227
237
  threadId: '话题 ID(多话题路由时)',
228
238
  chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
239
+ isTriggerTask: '当前任务是否来源于 Trigger(true/false)',
229
240
  proactivePreTool1stMsgChk: 'proactive 前置工具首消息检查(true/false)',
230
241
  proactiveToolUseReminder: 'proactive 工具调用提醒(true/false)',
231
242
  proactiveFirstSendRequired: '当前消息是否要求首次非发送工具前先发送表态',