evolcore 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -1,22 +1,21 @@
1
1
  import { formatTimestamp } from './session-fs-store.js';
2
2
  /** Default topic component used by the stable session routing key. */
3
3
  export const DEFAULT_THREAD_ID = 'main';
4
- /** Normalize the optional topic category. The public field is spelled `lable`. */
5
- export function normalizeTopicLable(value) {
4
+ export function normalizeTopicLabel(value) {
6
5
  if (value === undefined || value === null)
7
6
  return undefined;
8
7
  if (typeof value !== 'string')
9
- throw new Error('lable 必须是字符串或 null');
10
- const lable = value.trim();
11
- if (lable.length > 100 || /[\x00-\x1F\x7F]/.test(lable)) {
12
- throw new Error('lable 最多 100 个字符,且不能包含控制字符');
8
+ throw new Error('label 必须是字符串或 null');
9
+ const label = value.trim();
10
+ if (label.length > 100 || /[\x00-\x1F\x7F]/.test(label)) {
11
+ throw new Error('label 最多 100 个字符,且不能包含控制字符');
13
12
  }
14
- return lable || undefined;
13
+ return label || undefined;
15
14
  }
16
- /** The reserved category used to hide a topic from the default topic list. */
17
- export const ARCHIVED_TOPIC_LABLE = '归档';
18
- export function isArchivedTopicLable(lable) {
19
- return lable === ARCHIVED_TOPIC_LABLE;
15
+ export function normalizeTopicStatus(value) {
16
+ if (value === 'pending' || value === 'in_progress' || value === 'completed' || value === 'archived')
17
+ return value;
18
+ throw new Error('topicStatus 必须是 pending、in_progress、completed 或 archived');
20
19
  }
21
20
  /** Resolve the public archived query flag while keeping omitted values backward compatible. */
22
21
  export function resolveTopicArchiveFilter(value) {
@@ -28,13 +27,26 @@ export function resolveTopicArchiveFilter(value) {
28
27
  return 'all';
29
28
  throw new Error('archived 必须是布尔值或 "all"');
30
29
  }
31
- /** Ordinary messages may initialize a category; malformed metadata is ignored. */
32
- export function readTopicLable(payload) {
30
+ export function readTopicLabel(payload) {
33
31
  if (!payload || typeof payload !== 'object')
34
32
  return undefined;
35
33
  const fields = payload;
36
34
  try {
37
- return normalizeTopicLable(Object.hasOwn(fields, 'lable') ? fields.lable : fields.label);
35
+ const label = fields.label === undefined ? fields.lable : fields.label;
36
+ return label === '归档' && fields.label === undefined ? undefined : normalizeTopicLabel(label);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ export function readTopicStatus(payload) {
43
+ if (!payload || typeof payload !== 'object')
44
+ return undefined;
45
+ try {
46
+ const fields = payload;
47
+ if (fields.topicStatus === undefined && fields.lable === '归档')
48
+ return 'archived';
49
+ return normalizeTopicStatus(fields.topicStatus);
38
50
  }
39
51
  catch {
40
52
  return undefined;
@@ -103,7 +115,8 @@ export function sessionToFile(session) {
103
115
  projectPath: session.projectPath,
104
116
  agentSessionId: session.agentSessionId ?? null,
105
117
  name: session.name ?? null,
106
- ...(session.lable ? { lable: session.lable } : {}),
118
+ ...(session.label ? { label: session.label } : {}),
119
+ ...(session.threadId ? { topicStatus: session.topicStatus ?? 'pending' } : {}),
107
120
  activeTask: session.processingState ?? null,
108
121
  metadata,
109
122
  createdAt: session.createdAt,
@@ -153,7 +166,11 @@ export function fileToSession(file) {
153
166
  projectPath: file.projectPath,
154
167
  agentSessionId: file.agentSessionId ?? undefined,
155
168
  name: file.name ?? undefined,
156
- ...(file.lable ? { lable: file.lable } : {}),
169
+ ...(file.threadId && (file.label || (file.lable && file.lable !== '归档'))
170
+ ? { label: file.label || file.lable } : {}),
171
+ ...(file.threadId ? { topicStatus: file.topicStatus
172
+ ? normalizeTopicStatus(file.topicStatus)
173
+ : file.lable === '归档' ? 'archived' : 'pending' } : {}),
157
174
  processingState: file.activeTask ?? undefined,
158
175
  metadata,
159
176
  createdAt: file.createdAt,
@@ -1,25 +1,26 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { resolveEffective } from '../../config/config-manager.js';
2
4
  import { formatPeerKey } from '../relation/peer-identity.js';
3
5
  import { logger } from '../../utils/logger.js';
4
6
  import { isTransientProtocolMessage, messageLogPath } from '../message/message-log.js';
5
7
  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 { normalizeBaseagent } from '../../agents/baseagent.js';
8
+ import { getPackageRoot } from '../../paths.js';
9
9
  const DEFAULT_AFTER_HOURS = 24;
10
10
  const DEFAULT_FALLBACK = 'continue';
11
- const MAX_CONTEXT_MESSAGES = 40;
11
+ const DEFAULT_NEW_PROBABILITY_THRESHOLD = 0.85;
12
+ const MAX_CONTEXT_MESSAGES = 30;
13
+ const PRESERVED_HEAD_MESSAGES = 5;
14
+ const PRESERVED_TAIL_MESSAGES = 25;
12
15
  const MAX_CONTEXT_CHARS = 16_000;
13
- const MAX_MESSAGE_CHARS = 2_000;
16
+ const MAX_MESSAGE_CHARS = 800;
17
+ const MAX_MESSAGE_HEAD_CHARS = 600;
18
+ const MAX_MESSAGE_TAIL_CHARS = 200;
14
19
  const MODEL_TIMEOUT_MS = 10_000;
15
20
  const RECENT_DECISION_TTL_MS = 30_000;
16
21
  const FRESH_EMPTY_SESSION_TTL_MS = 30_000;
17
22
  const RENEWED_AT_METADATA_KEY = '__sessionRenewedAt';
18
23
  const CREATED_AT_METADATA_KEY = '__sessionCreatedAt';
19
- const NEW_CONFIDENCE_THRESHOLD = 0.85;
20
- const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
21
- const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
22
- const EXPLICIT_CONTINUE_RE = /^(?:继续上次|延续上次|继续|接着)[::,,;;。!!\n]/;
23
24
  const NON_CONVERSATION_TYPES = new Set([
24
25
  'command',
25
26
  'thought',
@@ -28,13 +29,59 @@ const NON_CONVERSATION_TYPES = new Set([
28
29
  'tool_call',
29
30
  'tool_result',
30
31
  ]);
31
- export function classifyExplicitSessionRenewSignal(content) {
32
- const normalized = content.trimStart();
33
- if (EXPLICIT_NEW_RE.test(normalized))
34
- return 'new';
35
- if (EXPLICIT_CONTINUE_RE.test(normalized))
36
- return 'continue';
37
- return undefined;
32
+ const SESSION_RENEW_TEMPLATE_PATH = path.join(getPackageRoot(), 'kits', 'templates', 'decision', 'session-renew.md');
33
+ const DEFAULT_SESSION_RENEW_PROMPT = {
34
+ stateRules: [
35
+ '会话边界判定规则:',
36
+ '1. 如果当前消息的内容主题已经转向另一件事,应判定为新会话,即使没有使用会话命令词。',
37
+ '2. 如果当前消息表达暂停当前主题并转向另一件事,也应判定为新会话。',
38
+ '3. 如果当前消息仍围绕历史主题补充、追问、修改或执行,应判定为继续当前会话。',
39
+ '4. 如果信息不足、无法可靠判断主题关系,应保留不确定性,不要默认新建或继续。',
40
+ '5. 重点判断内容主题和上下文关系,不要只匹配“新开”“重新”“换个”“继续”等关键词。',
41
+ ].join('\n'),
42
+ questionInstructions: '判断当前消息相对于历史会话是否应该开启独立的新会话,重点依据内容主题和上下文关系。',
43
+ trueCriteria: '当前消息已经转向另一件具体事情,或表达了停止当前主题、之后开始另一件事的边界意图。',
44
+ falseCriteria: '当前消息仍然围绕历史主题补充、追问、修改或执行。',
45
+ };
46
+ let cachedSessionRenewPrompt;
47
+ function parseSessionRenewPromptTemplate(source) {
48
+ const sections = new Map();
49
+ const matches = [...source.matchAll(/^##\s+(.+)\s*$/gmu)];
50
+ for (const [index, match] of matches.entries()) {
51
+ const name = match[1].trim();
52
+ const start = (match.index ?? 0) + match[0].length;
53
+ const end = matches[index + 1]?.index ?? source.length;
54
+ sections.set(name, source.slice(start, end).trim());
55
+ }
56
+ const prompt = {
57
+ stateRules: sections.get('State Rules'),
58
+ questionInstructions: sections.get('Question Instructions'),
59
+ trueCriteria: sections.get('True Criteria'),
60
+ falseCriteria: sections.get('False Criteria'),
61
+ };
62
+ return Object.values(prompt).every(value => value)
63
+ ? prompt
64
+ : undefined;
65
+ }
66
+ export function loadSessionRenewPrompt() {
67
+ if (cachedSessionRenewPrompt)
68
+ return cachedSessionRenewPrompt;
69
+ try {
70
+ const parsed = parseSessionRenewPromptTemplate(fs.readFileSync(SESSION_RENEW_TEMPLATE_PATH, 'utf8'));
71
+ if (parsed) {
72
+ cachedSessionRenewPrompt = parsed;
73
+ return parsed;
74
+ }
75
+ logger.warn(`[SessionRenewPrompt] Missing required sections: ${SESSION_RENEW_TEMPLATE_PATH}`);
76
+ }
77
+ catch (error) {
78
+ logger.warn(`[SessionRenewPrompt] Failed to load ${SESSION_RENEW_TEMPLATE_PATH}: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ cachedSessionRenewPrompt = DEFAULT_SESSION_RENEW_PROMPT;
81
+ return DEFAULT_SESSION_RENEW_PROMPT;
82
+ }
83
+ export function resetSessionRenewPromptForTests() {
84
+ cachedSessionRenewPrompt = undefined;
38
85
  }
39
86
  export function isSessionRenewConversationEntry(entry, sessionId) {
40
87
  return entry.sessionId === sessionId
@@ -43,26 +90,52 @@ export function isSessionRenewConversationEntry(entry, sessionId) {
43
90
  && entry.content.trim().length > 0;
44
91
  }
45
92
  export function selectSessionRenewContext(entries) {
46
- if (entries.length <= MAX_CONTEXT_MESSAGES)
47
- return trimContextByChars(entries);
48
- const selected = [entries[0], ...entries.slice(-(MAX_CONTEXT_MESSAGES - 1))];
49
- return trimContextByChars(selected);
93
+ const selected = entries.length <= MAX_CONTEXT_MESSAGES
94
+ ? entries
95
+ : [
96
+ ...entries.slice(0, PRESERVED_HEAD_MESSAGES),
97
+ ...entries.slice(-PRESERVED_TAIL_MESSAGES),
98
+ ];
99
+ return selected.map(entry => ({
100
+ ...entry,
101
+ content: truncateSessionRenewMessage(entry.content),
102
+ }));
50
103
  }
51
- function trimContextByChars(entries) {
52
- if (entries.length === 0)
53
- return [];
54
- const first = entries[0];
55
- const tail = [];
56
- let remaining = MAX_CONTEXT_CHARS - Math.min(first.content.length, MAX_MESSAGE_CHARS);
57
- for (let index = entries.length - 1; index >= 1 && remaining > 0; index--) {
58
- const entry = entries[index];
59
- const length = Math.min(entry.content.length, MAX_MESSAGE_CHARS);
60
- if (tail.length > 0 && length > remaining)
104
+ function truncateSessionRenewMessage(content) {
105
+ if (content.length <= MAX_MESSAGE_CHARS)
106
+ return content;
107
+ const marker = '[中间内容已省略]';
108
+ const available = MAX_MESSAGE_CHARS - marker.length;
109
+ const headBudget = Math.min(MAX_MESSAGE_HEAD_CHARS, available);
110
+ const tailBudget = Math.min(MAX_MESSAGE_TAIL_CHARS, available - headBudget);
111
+ const head = takeParagraphBoundary(content, headBudget, false);
112
+ const tail = takeParagraphBoundary(content, tailBudget, true);
113
+ return `${head}${marker}${tail}`.slice(0, MAX_MESSAGE_CHARS);
114
+ }
115
+ function takeParagraphBoundary(content, budget, fromEnd) {
116
+ if (budget <= 0)
117
+ return '';
118
+ if (content.length <= budget)
119
+ return content;
120
+ const paragraphs = content.split(/\n\s*\n/u).filter(Boolean);
121
+ if (paragraphs.length <= 1) {
122
+ return fromEnd ? content.slice(-budget) : content.slice(0, budget);
123
+ }
124
+ const selected = [];
125
+ let used = 0;
126
+ const ordered = fromEnd ? [...paragraphs].reverse() : paragraphs;
127
+ for (const paragraph of ordered) {
128
+ const separatorLength = selected.length > 0 ? 2 : 0;
129
+ if (used + separatorLength + paragraph.length > budget)
61
130
  break;
62
- tail.push(entry);
63
- remaining -= length;
131
+ selected.push(paragraph);
132
+ used += separatorLength + paragraph.length;
64
133
  }
65
- return [first, ...tail.reverse()];
134
+ if (selected.length === 0) {
135
+ return fromEnd ? content.slice(-budget) : content.slice(0, budget);
136
+ }
137
+ const result = fromEnd ? selected.reverse() : selected;
138
+ return result.join('\n\n');
66
139
  }
67
140
  export class SessionRenewService {
68
141
  sessionManager;
@@ -72,14 +145,12 @@ export class SessionRenewService {
72
145
  now;
73
146
  effectiveConfigResolver;
74
147
  eventBus;
75
- responseModeRegistry = new ResponseModeRegistry();
76
148
  constructor(sessionManager, inferenceProviderResolver, options = {}) {
77
149
  this.sessionManager = sessionManager;
78
150
  this.inferenceProviderResolver = inferenceProviderResolver;
79
151
  this.now = options.now ?? (() => Date.now());
80
152
  this.effectiveConfigResolver = options.resolveEffectiveConfig;
81
153
  this.eventBus = options.eventBus;
82
- registerBuiltinModes(this.responseModeRegistry);
83
154
  }
84
155
  async resolve(request) {
85
156
  const effectiveConfig = this.resolveEffectiveConfig(request);
@@ -107,6 +178,7 @@ export class SessionRenewService {
107
178
  renewed: cached.decision === 'new',
108
179
  decision: cached.decision,
109
180
  source: cached.source,
181
+ newProbability: cached.newProbability,
110
182
  };
111
183
  }
112
184
  }
@@ -144,47 +216,32 @@ export class SessionRenewService {
144
216
  const idleMs = this.now() - last.ts;
145
217
  if (idleMs <= afterHours * 60 * 60 * 1000)
146
218
  return { session: request.session, renewed: false };
147
- let source;
148
- let decision = classifyExplicitSessionRenewSignal(request.content);
149
- if (decision) {
150
- source = 'explicit';
151
- }
152
- else if (request.replyToMessageId
153
- && entries.some(entry => entry.msgId === request.replyToMessageId)) {
154
- decision = 'continue';
155
- source = 'reply';
219
+ const threshold = validProbability(config.new_probability_threshold) ?? DEFAULT_NEW_PROBABILITY_THRESHOLD;
220
+ let decision;
221
+ let source = 'model';
222
+ let judgeMetadata = {
223
+ model: effectiveConfig.decision_model?.model,
224
+ threshold,
225
+ };
226
+ try {
227
+ const model = effectiveConfig.decision_model?.model?.trim();
228
+ if (!model)
229
+ throw new SessionRenewJudgeError('decision_model_missing');
230
+ const provider = this.inferenceProviderResolver.getDecisionInferenceProvider(request.selfAid);
231
+ if (!provider)
232
+ throw new SessionRenewJudgeError('provider_unavailable');
233
+ const judged = await this.judgeWithModel(request, entries, idleMs, provider, model);
234
+ const newProbability = judged.newProbability;
235
+ decision = newProbability >= threshold ? 'new' : 'continue';
236
+ judgeMetadata = { ...judgeMetadata, newProbability };
156
237
  }
157
- else {
158
- const auxiliary = this.resolveAuxiliaryConfig(effectiveConfig, request.session.baseagent);
159
- let judgeMetadata = {
160
- responseMode: auxiliary.responseMode,
161
- baseagent: request.session.baseagent,
162
- model: auxiliary.model,
163
- effort: auxiliary.effort,
164
- };
165
- try {
166
- if (!auxiliary.model)
167
- throw new SessionRenewJudgeError('auxiliary_model_missing');
168
- if (auxiliary.invalidEffort)
169
- throw new SessionRenewJudgeError('auxiliary_effort_invalid');
170
- const provider = this.inferenceProviderResolver.getTextInferenceProvider(request.channelName, request.session.baseagent, request.selfAid);
171
- if (!provider)
172
- throw new SessionRenewJudgeError('provider_unavailable');
173
- const judged = await this.judgeWithModel(request, entries, idleMs, provider, auxiliary);
174
- decision = judged.decision === 'new' && judged.confidence < NEW_CONFIDENCE_THRESHOLD
175
- ? 'continue'
176
- : judged.decision;
177
- source = 'model';
178
- }
179
- catch (error) {
180
- decision = config.fallback_action ?? DEFAULT_FALLBACK;
181
- source = 'fallback';
182
- judgeMetadata = { ...judgeMetadata, fallbackReason: sessionRenewFallbackReason(error) };
183
- logger.warn(`[SessionRenew] judge failed, using fallback=${decision}: ${error instanceof Error ? error.message : String(error)}`);
184
- }
185
- return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judgeMetadata);
238
+ catch (error) {
239
+ decision = config.fallback_action ?? DEFAULT_FALLBACK;
240
+ source = 'fallback';
241
+ judgeMetadata = { ...judgeMetadata, fallbackReason: sessionRenewFallbackReason(error) };
242
+ logger.warn(`[SessionRenew] judge failed, using fallback=${decision}: ${error instanceof Error ? error.message : String(error)}`);
186
243
  }
187
- return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt);
244
+ return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judgeMetadata);
188
245
  }
189
246
  async finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judge) {
190
247
  const selected = decision === 'new'
@@ -200,14 +257,14 @@ export class SessionRenewService {
200
257
  selectedSessionId: selected.id,
201
258
  decision,
202
259
  source,
260
+ newProbability: judge?.newProbability,
203
261
  expiresAt: this.now() + RECENT_DECISION_TTL_MS,
204
262
  });
205
263
  logger.info(`[SessionRenew] session=${request.session.id} selected=${selected.id} decision=${decision} source=${source}`
206
264
  + ` idleHours=${(idleMs / 3_600_000).toFixed(2)}`
207
- + ` responseMode=${judge?.responseMode ?? 'none'}`
208
- + ` baseagent=${judge?.baseagent ?? 'none'}`
209
265
  + ` model=${judge?.model ?? 'none'}`
210
- + ` effort=${judge?.effort ?? 'provider-default'}`
266
+ + ` newProbability=${judge?.newProbability ?? 'none'}`
267
+ + ` threshold=${judge?.threshold ?? 'none'}`
211
268
  + ` fallbackReason=${judge?.fallbackReason ?? 'none'}`);
212
269
  this.eventBus?.publish({
213
270
  type: 'session:renew-evaluated',
@@ -221,16 +278,20 @@ export class SessionRenewService {
221
278
  channelId: request.channelId,
222
279
  selfAid: request.selfAid,
223
280
  idleHours: Number((idleMs / 3_600_000).toFixed(2)),
224
- responseMode: judge?.responseMode,
225
- baseagent: judge?.baseagent,
226
281
  model: judge?.model,
227
- effort: judge?.effort,
282
+ newProbability: judge?.newProbability,
228
283
  fallbackReason: judge?.fallbackReason,
229
284
  durationMs: Math.max(0, Date.now() - evaluationStartedAt),
230
285
  timestamp: this.now(),
231
286
  causation: request.causation,
232
287
  });
233
- return { session: selected, renewed: decision === 'new', decision, source };
288
+ return {
289
+ session: selected,
290
+ renewed: decision === 'new',
291
+ decision,
292
+ source,
293
+ newProbability: judge?.newProbability,
294
+ };
234
295
  }
235
296
  resolveEffectiveConfig(request) {
236
297
  if (this.effectiveConfigResolver)
@@ -246,76 +307,38 @@ export class SessionRenewService {
246
307
  peerKey,
247
308
  }, { cache: true, expand: true });
248
309
  }
249
- resolveAuxiliaryConfig(config, baseagent) {
250
- const canonicalBaseagent = normalizeBaseagent(baseagent).canonical;
251
- const baseagentConfig = canonicalBaseagent !== 'unknown'
252
- ? config.baseagents?.[canonicalBaseagent]
253
- : undefined;
254
- const rawBaseagentConfig = baseagentConfig && typeof baseagentConfig === 'object'
255
- ? baseagentConfig
256
- : undefined;
257
- // Auxiliary inference is owned exclusively by the selected baseagent. Old
258
- // response-mode bucket values must be migrated before they can take effect.
259
- const configuredMode = typeof config.responseMode === 'string' && config.responseMode.trim()
260
- ? config.responseMode.trim()
261
- : undefined;
262
- const responseMode = configuredMode && this.responseModeRegistry.has(configuredMode)
263
- ? configuredMode
264
- : this.responseModeRegistry.getPreferredId() ?? 'single-session';
265
- const rawModel = rawBaseagentConfig?.auxiliaryModel;
266
- const rawEffort = rawBaseagentConfig?.auxiliaryEffort;
267
- const model = typeof rawModel === 'string' && rawModel.trim()
268
- ? rawModel.trim()
269
- : undefined;
270
- const effort = typeof rawEffort === 'string' && AUXILIARY_EFFORTS.has(rawEffort)
271
- ? rawEffort
272
- : undefined;
273
- return {
274
- responseMode,
275
- model,
276
- effort,
277
- invalidEffort: rawEffort !== undefined && effort === undefined,
278
- };
279
- }
280
310
  readConversationEntries(session) {
281
311
  const entries = readAllJsonlLines(messageLogPath(this.sessionManager.getChatDir(session)));
282
312
  return entries
283
313
  .filter(entry => isSessionRenewConversationEntry(entry, session.id))
284
314
  .sort((left, right) => left.ts - right.ts);
285
315
  }
286
- async judgeWithModel(request, entries, idleMs, provider, auxiliary) {
287
- const context = selectSessionRenewContext(entries).map(entry => ({
288
- dir: entry.dir,
289
- content: entry.content.slice(0, MAX_MESSAGE_CHARS),
290
- ts: entry.ts,
291
- msgType: entry.msgType,
292
- }));
293
- const prompt = JSON.stringify({
294
- old_session_messages: context,
295
- new_message: request.content,
296
- reply_to_message_id: request.replyToMessageId || null,
297
- chat_type: request.chatType,
298
- idle_hours: Number((idleMs / 3_600_000).toFixed(2)),
299
- });
300
- const systemPrompt = [
301
- 'You are a session continuity classifier.',
302
- 'Decide whether the new message continues the old conversation or starts an independent conversation.',
303
- 'Use NEW only when the new intent is clearly independent. When uncertain, choose CONTINUE.',
304
- 'Do not call tools. Do not explain your reasoning.',
305
- 'Return exactly one JSON object: {"decision":"continue|new","confidence":0..1,"reason_code":"short_code"}.',
306
- ].join('\n');
316
+ async judgeWithModel(request, entries, idleMs, provider, model) {
317
+ const prompt = loadSessionRenewPrompt();
318
+ const state = {
319
+ content: buildSessionRenewStateContent(request, entries, idleMs),
320
+ };
321
+ const questions = {
322
+ new_session: {
323
+ type: 'noul',
324
+ instructions: prompt.questionInstructions,
325
+ criteria: {
326
+ true: prompt.trueCriteria,
327
+ false: prompt.falseCriteria,
328
+ },
329
+ },
330
+ };
307
331
  const controller = new AbortController();
308
332
  let timer;
309
333
  try {
310
334
  const operation = (async () => {
311
- const text = await provider.completeText({
312
- model: auxiliary.model,
313
- ...(auxiliary.effort ? { effort: auxiliary.effort } : {}),
314
- system: systemPrompt,
315
- input: prompt,
335
+ const text = await provider.completeDecision({
336
+ model,
337
+ state,
338
+ questions,
316
339
  signal: controller.signal,
317
340
  });
318
- return { model: auxiliary.model, text };
341
+ return text;
319
342
  })();
320
343
  const timeout = new Promise((_resolve, reject) => {
321
344
  timer = setTimeout(() => {
@@ -324,7 +347,7 @@ export class SessionRenewService {
324
347
  }, MODEL_TIMEOUT_MS);
325
348
  });
326
349
  const result = await Promise.race([operation, timeout]);
327
- return { ...parseModelResult(result.text), model: result.model };
350
+ return parseModelResult(result);
328
351
  }
329
352
  finally {
330
353
  if (timer)
@@ -367,29 +390,68 @@ function sessionRenewFallbackReason(error) {
367
390
  return 'timeout';
368
391
  if (/HTTP \d+/i.test(message))
369
392
  return 'http_error';
370
- if (/no JSON|invalid decision|invalid confidence/i.test(message))
393
+ if (/no JSON|invalid noul|invalid response/i.test(message))
371
394
  return 'invalid_response';
372
- if (/no text|empty/i.test(message))
395
+ if (/empty/i.test(message))
373
396
  return 'empty_response';
374
397
  return 'inference_error';
375
398
  }
376
399
  function validPositiveNumber(value) {
377
400
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
378
401
  }
402
+ function validProbability(value) {
403
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1 ? value : undefined;
404
+ }
405
+ function buildSessionRenewStateContent(request, entries, idleMs) {
406
+ const title = request.session.name?.trim() || '无标题';
407
+ const header = [
408
+ `话题标题:${title}`,
409
+ `当前聊天类型:${request.chatType}`,
410
+ `空闲时长:${Number((idleMs / 3_600_000).toFixed(2))} 小时`,
411
+ `是否回复旧消息:${request.replyToMessageId ? `是,消息 ID=${request.replyToMessageId}` : '否'}`,
412
+ loadSessionRenewPrompt().stateRules,
413
+ `当前新消息:${request.content}`,
414
+ '',
415
+ '历史会话上下文:',
416
+ ].join('\n');
417
+ const selected = selectSessionRenewContext(entries);
418
+ const historyHeader = `${header}\n`;
419
+ if (historyHeader.length >= MAX_CONTEXT_CHARS)
420
+ return historyHeader.slice(0, MAX_CONTEXT_CHARS);
421
+ const blocks = [];
422
+ for (const [index, entry] of selected.entries()) {
423
+ const block = [
424
+ `历史消息 ${index + 1}(${entry.dir},${entry.msgType},${new Date(entry.ts).toISOString()},消息 ID=${entry.msgId || '无'}):`,
425
+ entry.content,
426
+ ].join('\n');
427
+ const candidate = blocks.length > 0
428
+ ? `${blocks.join('\n\n')}\n\n${block}`
429
+ : block;
430
+ if (historyHeader.length + candidate.length <= MAX_CONTEXT_CHARS)
431
+ blocks.push(block);
432
+ }
433
+ return `${historyHeader}${blocks.join('\n\n')}`.slice(0, MAX_CONTEXT_CHARS);
434
+ }
379
435
  function parseModelResult(text) {
380
- const match = text.match(/\{[\s\S]*\}/);
381
- if (!match)
382
- throw new Error('judge returned no JSON object');
383
- const parsed = JSON.parse(match[0]);
384
- if (parsed.decision !== 'continue' && parsed.decision !== 'new') {
385
- throw new Error('judge returned invalid decision');
436
+ let parsed;
437
+ try {
438
+ parsed = JSON.parse(text);
386
439
  }
387
- if (typeof parsed.confidence !== 'number' || !Number.isFinite(parsed.confidence) || parsed.confidence < 0 || parsed.confidence > 1) {
388
- throw new Error('judge returned invalid confidence');
440
+ catch {
441
+ throw new Error('judge returned invalid response JSON');
389
442
  }
390
- return {
391
- decision: parsed.decision,
392
- confidence: parsed.confidence,
393
- reasonCode: typeof parsed.reason_code === 'string' ? parsed.reason_code : undefined,
394
- };
443
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
444
+ throw new Error('judge returned invalid response');
445
+ }
446
+ const answer = parsed.answers;
447
+ const newSession = answer && typeof answer === 'object' && !Array.isArray(answer)
448
+ ? answer.new_session
449
+ : undefined;
450
+ const noul = newSession && typeof newSession === 'object' && !Array.isArray(newSession)
451
+ ? newSession.noul
452
+ : undefined;
453
+ const newProbability = validProbability(noul);
454
+ if (newProbability === undefined)
455
+ throw new Error('judge returned invalid noul');
456
+ return { newProbability };
395
457
  }