evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -0,0 +1,454 @@
1
+ import path from 'path';
2
+ import { readGroupHistoryContext } from '../../aun/group/history.js';
3
+ import { getPackageRoot } from '../../paths.js';
4
+ import { createErrorCapturingReader, fileCache } from '../daemon-file-cache.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ const MAX_HISTORY_MESSAGES = 20;
7
+ const MAX_HISTORY_MESSAGE_CHARS = 800;
8
+ const MAX_STATE_CONTENT_CHARS = 16_000;
9
+ const HISTORY_HEAD_COUNT = 5;
10
+ const HISTORY_TAIL_COUNT = 15;
11
+ const ADMISSION_GRACE_MS = 5_000;
12
+ const DEFAULT_QUIET_WINDOW_MS = 3_000;
13
+ function parseExecutionPolicyTemplate(source) {
14
+ const sections = new Map();
15
+ const matches = [...source.matchAll(/^##[ \t]+([^\r\n]+)\r?$/gmu)];
16
+ for (const [index, match] of matches.entries()) {
17
+ const name = match[1].trim();
18
+ if (sections.has(name))
19
+ throw new Error(`Duplicate attention prompt section: ${name}`);
20
+ const start = (match.index ?? 0) + match[0].length;
21
+ const end = matches[index + 1]?.index ?? source.length;
22
+ sections.set(name, source.slice(start, end).trim());
23
+ }
24
+ const requiredSection = (name) => {
25
+ const content = sections.get(name);
26
+ if (!content)
27
+ throw new Error(`Missing or empty attention prompt section: ${name}`);
28
+ return content;
29
+ };
30
+ return {
31
+ execution_policy: {
32
+ type: 'choice',
33
+ instructions: requiredSection('Instructions'),
34
+ criteria: {
35
+ interrupt: requiredSection('Interrupt Criteria'),
36
+ defer: requiredSection('Defer Criteria'),
37
+ ignore: requiredSection('Ignore Criteria'),
38
+ },
39
+ },
40
+ };
41
+ }
42
+ function decisionFromHistory(entry) {
43
+ return entry.decision;
44
+ }
45
+ function lifecycleForHistory(entry, item, resolveMessageState) {
46
+ const decision = decisionFromHistory(entry);
47
+ if (decision === 'ignore')
48
+ return 'consumed';
49
+ if (!entry.message_id || !resolveMessageState)
50
+ return entry.lifecycle;
51
+ const state = resolveMessageState({
52
+ value: undefined,
53
+ selfAid: item.selfAid,
54
+ groupId: item.groupId,
55
+ threadId: item.threadId,
56
+ channelKey: item.channelKey,
57
+ sessionId: entry.session_id ?? item.sessionId,
58
+ projectPath: item.projectPath,
59
+ messageId: entry.message_id,
60
+ sender: entry.sender,
61
+ role: entry.role,
62
+ mentions: [],
63
+ content: entry.content,
64
+ receivedAt: Date.parse(entry.timestamp),
65
+ });
66
+ return state === 'queued' ? 'pending' : state === 'active' ? 'processing' : 'consumed';
67
+ }
68
+ function withLifecycle(entry, lifecycle) {
69
+ return { ...entry, lifecycle };
70
+ }
71
+ function dedupeMessages(messages) {
72
+ const seen = new Set();
73
+ return messages.filter(message => {
74
+ const key = message.message_id || `${message.timestamp}\n${message.sender}\n${message.content}`;
75
+ if (seen.has(key))
76
+ return false;
77
+ seen.add(key);
78
+ return true;
79
+ });
80
+ }
81
+ function chooseRecent(messages) {
82
+ const ordered = dedupeMessages(messages).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
83
+ if (ordered.length <= MAX_HISTORY_MESSAGES)
84
+ return ordered;
85
+ return [...ordered.slice(0, HISTORY_HEAD_COUNT), ...ordered.slice(-HISTORY_TAIL_COUNT)];
86
+ }
87
+ function compactHistoryContent(content, maxChars = MAX_HISTORY_MESSAGE_CHARS) {
88
+ if (content.length <= maxChars)
89
+ return content;
90
+ const marker = '[中间内容已省略]';
91
+ const paragraphs = content.split(/\n\s*\n/).map(value => value.trim()).filter(Boolean);
92
+ const headBudget = 600;
93
+ const tailBudget = Math.max(0, maxChars - headBudget - marker.length - 2);
94
+ if (paragraphs.length > 1) {
95
+ const head = [];
96
+ let headLength = 0;
97
+ for (const paragraph of paragraphs) {
98
+ if (headLength + paragraph.length + (head.length ? 2 : 0) > headBudget)
99
+ break;
100
+ head.push(paragraph);
101
+ headLength += paragraph.length + (head.length > 1 ? 2 : 0);
102
+ }
103
+ const tail = [];
104
+ let tailLength = 0;
105
+ for (let index = paragraphs.length - 1; index >= 0; index--) {
106
+ const paragraph = paragraphs[index];
107
+ if (tailLength + paragraph.length + (tail.length ? 2 : 0) > tailBudget)
108
+ break;
109
+ tail.unshift(paragraph);
110
+ tailLength += paragraph.length + (tail.length > 1 ? 2 : 0);
111
+ }
112
+ if (head.length > 0 && tail.length > 0 && head.join('\n\n') !== tail.join('\n\n')) {
113
+ const compacted = `${head.join('\n\n')}\n${marker}\n${tail.join('\n\n')}`;
114
+ if (compacted.length <= maxChars)
115
+ return compacted;
116
+ }
117
+ }
118
+ const head = Math.max(1, Math.floor((maxChars - marker.length) * 0.75));
119
+ const tail = Math.max(0, maxChars - marker.length - head);
120
+ return `${content.slice(0, head)}${marker}${tail ? content.slice(-tail) : ''}`.slice(0, maxChars);
121
+ }
122
+ function formatMessage(message, lifecycle, decision, compact = true) {
123
+ const sender = message.sender || '<unknown>';
124
+ const role = message.role || 'unknown';
125
+ const timestamp = 'timestamp' in message ? message.timestamp : '';
126
+ const body = compact ? compactHistoryContent(message.content) : message.content;
127
+ return `[lifecycle=${lifecycle} decision=${decision ?? 'none'} sender=${sender} role=${role} time=${timestamp}]\n${body}`;
128
+ }
129
+ function section(title, body) {
130
+ return `${title}\n${body || '(无)'}`;
131
+ }
132
+ function formatResources(resources) {
133
+ return [
134
+ section('当前群规则', resources.groupRules ?? ''),
135
+ section('当前 Agent 的个人资料', resources.personal ?? ''),
136
+ section('当前项目适用的项目指令', resources.projectInstructions ?? ''),
137
+ ].join('\n\n');
138
+ }
139
+ function buildStateContent(processingNow, recentMessages, pendingMessages, newMessages, resources) {
140
+ const fixed = [
141
+ `当前处理状态:${processingNow ? '当前 Agent 正在处理其他任务。' : '当前 Agent 当前没有正在处理的任务。'}`,
142
+ section('尚未完成处理的消息', pendingMessages.map(message => formatMessage(message, message.lifecycle, message.decision, false)).join('\n\n')),
143
+ section('本次新消息', newMessages.map(message => formatMessage(message, 'new', null, false)).join('\n\n')),
144
+ formatResources(resources),
145
+ ].join('\n\n');
146
+ const historyPrefix = '当前群会话历史对话\n';
147
+ const candidates = chooseRecent(recentMessages);
148
+ let kept = [...candidates];
149
+ const renderHistory = () => section(historyPrefix.trim(), kept.map(message => formatMessage(message, message.lifecycle, message.decision)).join('\n\n'));
150
+ while (`${fixed}\n\n${renderHistory()}`.length > MAX_STATE_CONTENT_CHARS && kept.length > 0) {
151
+ const removeIndex = kept.length > HISTORY_HEAD_COUNT ? HISTORY_HEAD_COUNT : 0;
152
+ kept.splice(removeIndex, 1);
153
+ }
154
+ const content = `${fixed}\n\n${renderHistory()}`;
155
+ if (content.length > MAX_STATE_CONTENT_CHARS) {
156
+ logger.warn(`[AutoMentionGate] required attention state exceeds ${MAX_STATE_CONTENT_CHARS} characters; preserving pending and new messages`);
157
+ }
158
+ return content;
159
+ }
160
+ export function parseAttentionDecision(text) {
161
+ return parseAttentionDecisionDetails(text).decision;
162
+ }
163
+ export function parseAttentionDecisionDetails(text) {
164
+ const parsed = JSON.parse(text.trim());
165
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
166
+ throw new Error('Invalid attention result');
167
+ const payload = parsed;
168
+ const answers = payload.answers;
169
+ const answer = answers && typeof answers === 'object' && !Array.isArray(answers)
170
+ ? answers.execution_policy
171
+ : undefined;
172
+ const rawAnswer = answer ?? payload.execution_policy ?? payload.decision;
173
+ const choice = typeof rawAnswer === 'string'
174
+ ? { choice: rawAnswer }
175
+ : rawAnswer && typeof rawAnswer === 'object' && !Array.isArray(rawAnswer)
176
+ ? rawAnswer
177
+ : undefined;
178
+ if (!choice)
179
+ throw new Error('Missing execution policy');
180
+ if (choice.type !== undefined && choice.type !== 'choice')
181
+ throw new Error('Invalid execution policy type');
182
+ const decision = choice.choice ?? choice.decision ?? rawAnswer;
183
+ if (decision !== 'interrupt' && decision !== 'defer' && decision !== 'ignore') {
184
+ throw new Error('Invalid attention decision');
185
+ }
186
+ const confidence = choice.confidence;
187
+ const probabilities = choice.probabilities;
188
+ const requestId = payload.request_id;
189
+ const sanitized = {};
190
+ if (probabilities !== undefined) {
191
+ if (!probabilities || typeof probabilities !== 'object' || Array.isArray(probabilities)) {
192
+ throw new Error('Invalid attention probabilities');
193
+ }
194
+ for (const [policy, value] of Object.entries(probabilities)) {
195
+ if (policy !== 'interrupt' && policy !== 'defer' && policy !== 'ignore') {
196
+ throw new Error('Unexpected attention probability');
197
+ }
198
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
199
+ throw new Error('Invalid attention probability');
200
+ }
201
+ sanitized[policy] = value;
202
+ }
203
+ }
204
+ if (confidence !== undefined && (typeof confidence !== 'number' || !Number.isFinite(confidence)
205
+ || confidence < 0 || confidence > 1))
206
+ throw new Error('Invalid attention confidence');
207
+ return {
208
+ decision,
209
+ ...(typeof confidence === 'number' ? { confidence } : {}),
210
+ ...(probabilities !== undefined ? { probabilities: sanitized } : {}),
211
+ ...(typeof requestId === 'string' && requestId ? { requestId: requestId.slice(0, 128) } : {}),
212
+ };
213
+ }
214
+ export class AutoMentionGate {
215
+ options;
216
+ states = new Map();
217
+ lastValidQuestions;
218
+ constructor(options) {
219
+ this.options = options;
220
+ }
221
+ loadExecutionPolicyQuestions() {
222
+ const templatePath = path.join(getPackageRoot(), 'kits', 'templates', 'decision', 'auto-mention.md');
223
+ const reader = createErrorCapturingReader();
224
+ try {
225
+ const questions = fileCache.get(templatePath, raw => {
226
+ if (raw === null) {
227
+ throw new Error(`Failed to read attention prompt ${templatePath}: ${reader.lastError()}`, {
228
+ cause: reader.lastErrorCause(),
229
+ });
230
+ }
231
+ return parseExecutionPolicyTemplate(raw);
232
+ }, { policy: 'mtime', group: 'decision-prompts', read: reader.read });
233
+ this.lastValidQuestions = questions;
234
+ return questions;
235
+ }
236
+ catch (error) {
237
+ if (!this.lastValidQuestions)
238
+ throw error;
239
+ logger.warn(`[AutoMentionGate] prompt template unavailable, using last valid prompt: ${String(error)}`);
240
+ return this.lastValidQuestions;
241
+ }
242
+ }
243
+ background(item, excludeMessageIds = []) {
244
+ try {
245
+ return (this.options.readContext ?? readGroupHistoryContext)({
246
+ from: item.selfAid,
247
+ groupId: item.groupId,
248
+ thread: item.threadId || 'main',
249
+ channelKey: item.channelKey,
250
+ excludeMessageIds,
251
+ maxMessages: MAX_HISTORY_MESSAGES,
252
+ });
253
+ }
254
+ catch (error) {
255
+ logger.warn(`[AutoMentionGate] history unavailable, continuing without background: ${String(error)}`);
256
+ return [];
257
+ }
258
+ }
259
+ async offer(item) {
260
+ const key = JSON.stringify([item.selfAid, item.groupId, item.threadId || 'main']);
261
+ let state = this.states.get(key);
262
+ if (!state) {
263
+ state = { pending: [], admitted: [], processing: false };
264
+ this.states.set(key, state);
265
+ }
266
+ if (state.maintenanceTimer) {
267
+ clearTimeout(state.maintenanceTimer);
268
+ state.maintenanceTimer = undefined;
269
+ }
270
+ this.pruneAdmitted(state);
271
+ const currentState = state;
272
+ const result = new Promise(resolve => {
273
+ currentState.pending.push({ item, queuedAt: Date.now(), resolve });
274
+ });
275
+ this.schedule(key, currentState);
276
+ return result;
277
+ }
278
+ pruneAdmitted(state) {
279
+ const now = Date.now();
280
+ state.admitted = state.admitted.filter(entry => {
281
+ if (!entry.item.messageId || !this.options.resolveMessageState) {
282
+ return now - entry.admittedAt < ADMISSION_GRACE_MS;
283
+ }
284
+ const messageState = this.options.resolveMessageState(entry.item);
285
+ if (messageState === 'queued')
286
+ return true;
287
+ if (messageState === 'active')
288
+ return false;
289
+ // A decision is resolved before the bridge persists and enqueues the
290
+ // message. Keep the in-memory copy briefly so the next classification
291
+ // cannot lose it during that admission gap.
292
+ return now - entry.admittedAt < ADMISSION_GRACE_MS;
293
+ });
294
+ }
295
+ scheduleMaintenance(key, state) {
296
+ if (state.maintenanceTimer)
297
+ clearTimeout(state.maintenanceTimer);
298
+ state.maintenanceTimer = setTimeout(() => {
299
+ state.maintenanceTimer = undefined;
300
+ this.pruneAdmitted(state);
301
+ if (state.pending.length > 0 || state.processing)
302
+ return;
303
+ if (state.admitted.length > 0) {
304
+ this.scheduleMaintenance(key, state);
305
+ }
306
+ else if (this.states.get(key) === state) {
307
+ this.states.delete(key);
308
+ }
309
+ }, ADMISSION_GRACE_MS);
310
+ }
311
+ schedule(key, state) {
312
+ if (state.timer)
313
+ clearTimeout(state.timer);
314
+ state.timer = undefined;
315
+ if (state.processing || state.pending.length === 0)
316
+ return;
317
+ const latestPending = state.pending.at(-1);
318
+ const latestItem = latestPending.item;
319
+ const configuredQuietWindowMs = this.options.resolveQuietWindowMs?.(latestItem) ?? this.options.quietWindowMs;
320
+ const quietWindowMs = typeof configuredQuietWindowMs === 'number'
321
+ && Number.isFinite(configuredQuietWindowMs)
322
+ && configuredQuietWindowMs >= 0
323
+ ? configuredQuietWindowMs
324
+ : DEFAULT_QUIET_WINDOW_MS;
325
+ state.timer = setTimeout(() => {
326
+ state.timer = undefined;
327
+ void this.flush(key, state);
328
+ }, Math.max(0, latestPending.queuedAt + quietWindowMs - Date.now()));
329
+ }
330
+ async flush(key, state) {
331
+ if (state.processing || state.pending.length === 0)
332
+ return;
333
+ state.processing = true;
334
+ const batch = state.pending.splice(0, state.pending.length);
335
+ const items = batch.map(pending => pending.item);
336
+ const first = items[0];
337
+ const current = items.map(item => ({
338
+ source: 'current',
339
+ message_id: item.messageId ?? null,
340
+ sender: item.sender,
341
+ role: item.role,
342
+ mentions: item.mentions,
343
+ timestamp: new Date(item.receivedAt).toISOString(),
344
+ content: item.content,
345
+ }));
346
+ const history = this.readAllHistory(first, items.flatMap(item => item.messageId ? [item.messageId] : []));
347
+ const recent = history.map(entry => withLifecycle(entry, lifecycleForHistory(entry, first, this.options.resolveMessageState)));
348
+ const pendingHistory = recent.filter(entry => entry.lifecycle === 'pending');
349
+ const pendingItems = state.admitted.map(entry => this.asPendingMessage(entry.item, entry.decision));
350
+ const pendingMessages = dedupeMessages([...pendingHistory, ...pendingItems]);
351
+ let stateSnapshot;
352
+ let details = { decision: 'ignore' };
353
+ let source = 'fallback';
354
+ const processingNow = this.options.resolveProcessing?.(first) ?? !!first.mainSessionBusy;
355
+ try {
356
+ const inference = this.options.resolveInference(first);
357
+ if (!inference?.model)
358
+ throw new Error('Decision Model is not configured');
359
+ const resources = await this.options.resolveResources?.(first) ?? {};
360
+ const recentMessages = recent.filter(entry => entry.lifecycle !== 'pending');
361
+ const content = buildStateContent(processingNow, recentMessages, pendingMessages, current, resources);
362
+ stateSnapshot = {
363
+ processingNow,
364
+ recentMessages: chooseRecent(recentMessages),
365
+ pendingMessages,
366
+ newMessages: current,
367
+ resources,
368
+ content,
369
+ };
370
+ const questions = this.loadExecutionPolicyQuestions();
371
+ const controller = new AbortController();
372
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 3000);
373
+ try {
374
+ const response = await Promise.race([
375
+ inference.provider.completeDecision({
376
+ model: inference.model,
377
+ state: { content },
378
+ questions,
379
+ signal: controller.signal,
380
+ }),
381
+ new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error('Attention inference timeout')), { once: true })),
382
+ ]);
383
+ details = parseAttentionDecisionDetails(response);
384
+ source = 'model';
385
+ }
386
+ finally {
387
+ clearTimeout(timeout);
388
+ controller.abort();
389
+ }
390
+ }
391
+ catch (error) {
392
+ details = { decision: 'ignore' };
393
+ source = 'fallback';
394
+ logger.warn(`[AutoMentionGate] decision model unavailable, falling back to mention-only: ${String(error)}`);
395
+ }
396
+ if (details.decision !== 'ignore') {
397
+ for (const item of items) {
398
+ if (!state.admitted.some(existing => existing.item.messageId && item.messageId
399
+ ? existing.item.messageId === item.messageId
400
+ : existing.item === item)) {
401
+ state.admitted.push({ item, decision: details.decision, admittedAt: Date.now() });
402
+ }
403
+ }
404
+ }
405
+ batch.forEach((pending, index) => pending.resolve({
406
+ decision: details.decision,
407
+ source,
408
+ leader: index === batch.length - 1,
409
+ batch: items,
410
+ background: stateSnapshot?.recentMessages ?? chooseRecent(recent),
411
+ current,
412
+ state: stateSnapshot,
413
+ details,
414
+ }));
415
+ state.processing = false;
416
+ if (state.pending.length) {
417
+ this.schedule(key, state);
418
+ }
419
+ else {
420
+ this.scheduleMaintenance(key, state);
421
+ }
422
+ }
423
+ readAllHistory(item, excludeMessageIds) {
424
+ try {
425
+ return (this.options.readContext ?? readGroupHistoryContext)({
426
+ from: item.selfAid,
427
+ groupId: item.groupId,
428
+ thread: item.threadId || 'main',
429
+ channelKey: item.channelKey,
430
+ excludeMessageIds,
431
+ maxMessages: Number.MAX_SAFE_INTEGER,
432
+ maxChars: Number.MAX_SAFE_INTEGER,
433
+ });
434
+ }
435
+ catch (error) {
436
+ logger.warn(`[AutoMentionGate] history unavailable, continuing without background: ${String(error)}`);
437
+ return [];
438
+ }
439
+ }
440
+ asPendingMessage(item, decision) {
441
+ return {
442
+ source: 'background',
443
+ message_id: item.messageId ?? null,
444
+ session_id: item.sessionId,
445
+ sender: item.sender,
446
+ role: item.role,
447
+ timestamp: new Date(item.receivedAt).toISOString(),
448
+ content: item.content,
449
+ lifecycle: 'pending',
450
+ decision,
451
+ msg_type: 'text',
452
+ };
453
+ }
454
+ }
@@ -3,7 +3,7 @@ import { summarizeToolInput, summarizeToolInputForDisplay, toolInputForDisplay }
3
3
  import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
4
4
  import { tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
5
5
  import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
6
- import { classifyAunToolErrorCode } from '../permission/tool-error-code.js';
6
+ import { classifyAunToolErrorCode, normalizeToolErrorClassification } from '../permission/tool-error-code.js';
7
7
  export class IMRenderer {
8
8
  opts;
9
9
  itemsQueue = [];
@@ -21,6 +21,7 @@ export class IMRenderer {
21
21
  /** 自增 callId 兜底(runner 没提供时用) */
22
22
  syntheticCallSeq = 0;
23
23
  currentAssistantUuid;
24
+ currentTextPhase;
24
25
  constructor(opts) {
25
26
  this.opts = opts;
26
27
  }
@@ -175,14 +176,24 @@ export class IMRenderer {
175
176
  }
176
177
  // ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
177
178
  /** 添加文本片段(流式 text) */
178
- async addText(text, outputTokens, turn, assistantUuid) {
179
+ async addText(text, outputTokens, turn, assistantUuid, phase) {
179
180
  if (this.opts.envelope.chatmode === 'proactive')
180
181
  return;
181
- if (this.textBuffer && this.currentAssistantUuid !== assistantUuid && !this.suppressIntermediateText()) {
182
+ const sameAssistant = this.currentAssistantUuid === assistantUuid;
183
+ // In deferred AUN private delivery, only explicit text_start/tool_use
184
+ // handlers are semantic boundaries. assistantUuid is correlation data and
185
+ // may be absent or change without proving that the prior text is final.
186
+ const hadPendingText = this.textBuffer.length > 0;
187
+ if (hadPendingText && !this.opts.deferTextUntilBoundary && !sameAssistant
188
+ && !this.suppressIntermediateText()) {
182
189
  await this.flushText();
183
190
  }
184
191
  // Do not carry the previous item's anchor onto unanchored output.
185
192
  this.currentAssistantUuid = assistantUuid;
193
+ // A mixed buffer has no trustworthy phase, even when its latest chunk is commentary.
194
+ this.currentTextPhase = hadPendingText && this.opts.deferTextUntilBoundary
195
+ ? this.currentTextPhase === phase ? phase : undefined
196
+ : phase;
186
197
  this.emitProgress('text', outputTokens, turn);
187
198
  if (!text)
188
199
  return;
@@ -201,41 +212,67 @@ export class IMRenderer {
201
212
  this.scheduleFlush();
202
213
  }
203
214
  /** 添加工具调用 */
204
- addToolCall(name, input, callId, descText, turn, outputTokens) {
215
+ addToolCall(name, input, callId, descText, turn, outputTokens, assistantUuid) {
205
216
  if (this.opts.envelope.chatmode === 'proactive')
206
217
  return;
207
218
  this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
208
- if (this.suppressActivityItems())
209
- return;
210
219
  const displayInput = toolInputForDisplay(name, input);
211
- this.itemsQueue.push({
220
+ // If this is a shell tool executing an EC command, extract the subcommand
221
+ // for the ext.evolCommand hint so the frontend can render it distinctly.
222
+ let evolCommand;
223
+ if ((name === 'Bash' || name === 'Shell') && typeof input?.command === 'string') {
224
+ const m = input.command.trim().match(/^(?:ec|evolcore)\s+([a-z][a-z0-9-]*)/i);
225
+ if (m)
226
+ evolCommand = m[1].toLowerCase();
227
+ }
228
+ const item = {
212
229
  kind: 'tool_call',
213
230
  call_id: callId || this.synthCallId(),
214
231
  name,
215
232
  arguments: displayInput,
216
233
  text: displayInput !== input ? summarizeToolInput(name, displayInput || {}) : descText,
217
- });
234
+ ...(evolCommand ? { evolCommand } : {}),
235
+ ...(assistantUuid ? { forkAnchor: { assistantMessageId: assistantUuid } } : {}),
236
+ };
237
+ this.recordActivity(item);
238
+ if (this.suppressActivityItems())
239
+ return;
240
+ this.itemsQueue.push(item);
218
241
  this.messageTimestamps.push(Date.now());
219
242
  this.scheduleFlush();
220
243
  }
221
244
  /** 添加工具结果 */
222
- addToolResult(name, ok, result, error, callId, durationMs, descText, classification) {
245
+ addToolResult(name, ok, result, error, callId, durationMs, descText, classification, assistantUuid) {
223
246
  if (this.opts.envelope.chatmode === 'proactive')
224
247
  return;
225
248
  this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
226
- if (this.suppressActivityItems())
227
- return;
228
- this.itemsQueue.push({
249
+ const normalizedClassification = !ok
250
+ ? normalizeToolErrorClassification(classification ?? {})
251
+ : undefined;
252
+ const item = {
229
253
  kind: 'tool_result',
230
254
  call_id: callId || this.synthCallId(),
231
255
  name,
232
256
  ok,
233
257
  ...(result !== undefined && { result }),
234
258
  ...(error !== undefined && { error }),
235
- ...(!ok && { error_code: classifyAunToolErrorCode(classification ?? {}) }),
259
+ ...(!ok && {
260
+ error_code: classifyAunToolErrorCode({
261
+ ...(classification ?? {}),
262
+ ...(normalizedClassification ?? {}),
263
+ }),
264
+ ...(normalizedClassification?.errorDetailCode
265
+ ? { detail_code: normalizedClassification.errorDetailCode }
266
+ : {}),
267
+ }),
236
268
  ...(durationMs !== undefined && { duration_ms: durationMs }),
237
269
  ...(descText !== undefined && { text: descText }),
238
- });
270
+ ...(assistantUuid ? { forkAnchor: { assistantMessageId: assistantUuid } } : {}),
271
+ };
272
+ this.recordActivity(item);
273
+ if (this.suppressActivityItems())
274
+ return;
275
+ this.itemsQueue.push(item);
239
276
  this.messageTimestamps.push(Date.now());
240
277
  this.scheduleFlush();
241
278
  }
@@ -276,6 +313,8 @@ export class IMRenderer {
276
313
  }
277
314
  /** Notify the client that a new model text block has started. */
278
315
  async sendTextStart(contentIndex, turn) {
316
+ if (this.opts.deferTextUntilBoundary)
317
+ await this.flushText();
279
318
  const payload = {
280
319
  kind: 'status.progress',
281
320
  metadata: { activityType: 'text', kind: 'text_start', contentIndex, turn },
@@ -305,6 +344,7 @@ export class IMRenderer {
305
344
  if (this.suppressActivityItems())
306
345
  return;
307
346
  this.itemsQueue.push(item);
347
+ this.recordActivity(item);
308
348
  this.messageTimestamps.push(Date.now());
309
349
  this.scheduleFlush();
310
350
  }
@@ -332,7 +372,9 @@ export class IMRenderer {
332
372
  }
333
373
  if (this.suppressActivityItems() && !force)
334
374
  return;
335
- this.itemsQueue.push({ kind: 'notice', text, severity, subtype });
375
+ const item = { kind: 'notice', text, severity, subtype };
376
+ this.itemsQueue.push(item);
377
+ this.recordActivity(item);
336
378
  this.messageTimestamps.push(Date.now());
337
379
  this.scheduleFlush();
338
380
  }
@@ -453,7 +495,7 @@ export class IMRenderer {
453
495
  const elapsed = Date.now() - this.lastFlush;
454
496
  const delay = Math.max(0, targetDelay - elapsed);
455
497
  this.timer = setTimeout(() => {
456
- void this.flushInternal().catch(error => {
498
+ void this.flushInternal(undefined, undefined, true).catch(error => {
457
499
  logger.warn(`[IMRenderer] scheduled flush failed: ${error instanceof Error ? error.message : String(error)}`);
458
500
  });
459
501
  }, delay);
@@ -485,7 +527,12 @@ export class IMRenderer {
485
527
  this.itemsQueue = this.itemsQueue.filter(it => it.kind === 'text');
486
528
  if (this.suppressActivityItems())
487
529
  return;
488
- const payload = { kind: 'activity.batch', items: nonThinking };
530
+ // If any tool_call item carries an EC subcommand hint, surface it on the batch payload ext.
531
+ const evolCommandItem = nonThinking.find(it => it.kind === 'tool_call' && !!it.evolCommand);
532
+ const evolCommand = evolCommandItem ? evolCommandItem.evolCommand : undefined;
533
+ const payload = evolCommand
534
+ ? { kind: 'activity.batch', items: nonThinking, ext: { evolCommand } }
535
+ : { kind: 'activity.batch', items: nonThinking };
489
536
  this.sentContent = true;
490
537
  try {
491
538
  await this.sendPayload(payload);
@@ -501,7 +548,7 @@ export class IMRenderer {
501
548
  * 完整 flush:把 itemsQueue 里所有 items 打包成 activity.batch 发送。
502
549
  * 如果 isFinal=true,还会在 batch 之后单独发一条 result.text 作为最终回复。
503
550
  */
504
- async flushInternal(isFinal, attention) {
551
+ async flushInternal(isFinal, attention, scheduled = false) {
505
552
  if (this.timer) {
506
553
  clearTimeout(this.timer);
507
554
  this.timer = undefined;
@@ -562,6 +609,10 @@ export class IMRenderer {
562
609
  if (!isFinal && this.textBuffer.length > 0) {
563
610
  if (this.suppressIntermediateText())
564
611
  return;
612
+ // A timer cannot decide whether this is the final answer. Keep it for
613
+ // an explicit boundary; known commentary can still be sent immediately.
614
+ if (scheduled && this.opts.deferTextUntilBoundary && this.currentTextPhase !== 'commentary')
615
+ return;
565
616
  const text = this.textBuffer;
566
617
  this.textBuffer = '';
567
618
  const payload = {
@@ -669,8 +720,25 @@ export class IMRenderer {
669
720
  this.emitProactiveItem(item);
670
721
  }
671
722
  /** proactive 模式逐条投影:单个 ThoughtItem 包成 activity.batch[1] 发出(fire-and-forget)。 */
723
+ recordActivity(item) {
724
+ try {
725
+ const result = this.opts.recordActivity?.(item);
726
+ if (result && typeof result.catch === 'function') {
727
+ result.catch(error => {
728
+ logger.warn(`[IMRenderer] task activity record failed: ${error instanceof Error ? error.message : String(error)}`);
729
+ });
730
+ }
731
+ }
732
+ catch (error) {
733
+ logger.warn(`[IMRenderer] task activity record failed: ${error instanceof Error ? error.message : String(error)}`);
734
+ }
735
+ }
672
736
  emitProactiveItem(item) {
673
- const payload = { kind: 'activity.batch', items: [item] };
737
+ this.recordActivity(item);
738
+ const evolCommand = item.kind === 'tool_call' ? item.evolCommand : undefined;
739
+ const payload = evolCommand
740
+ ? { kind: 'activity.batch', items: [item], ext: { evolCommand } }
741
+ : { kind: 'activity.batch', items: [item] };
674
742
  this.opts.send(payload).catch(err => {
675
743
  logger.debug(`[IMRenderer] proactive send failed: ${err.message}`);
676
744
  });
@@ -695,12 +763,14 @@ export class IMRenderer {
695
763
  }
696
764
  case 'tool_result':
697
765
  if (event.isError) {
766
+ const classification = normalizeToolErrorClassification(event);
698
767
  return {
699
768
  kind: 'tool_result',
700
769
  call_id: event.callId || this.synthCallId(),
701
770
  name: event.name,
702
771
  ok: false,
703
- error_code: classifyAunToolErrorCode(event),
772
+ error_code: classifyAunToolErrorCode({ ...event, ...classification }),
773
+ ...(classification.errorDetailCode ? { detail_code: classification.errorDetailCode } : {}),
704
774
  error: event.error || (typeof event.result === 'string' ? event.result : '执行失败'),
705
775
  ...(event.durationMs !== undefined && { duration_ms: event.durationMs }),
706
776
  };