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
@@ -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 = [];
@@ -212,41 +212,67 @@ export class IMRenderer {
212
212
  this.scheduleFlush();
213
213
  }
214
214
  /** 添加工具调用 */
215
- addToolCall(name, input, callId, descText, turn, outputTokens) {
215
+ addToolCall(name, input, callId, descText, turn, outputTokens, assistantUuid) {
216
216
  if (this.opts.envelope.chatmode === 'proactive')
217
217
  return;
218
218
  this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
219
- if (this.suppressActivityItems())
220
- return;
221
219
  const displayInput = toolInputForDisplay(name, input);
222
- 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 = {
223
229
  kind: 'tool_call',
224
230
  call_id: callId || this.synthCallId(),
225
231
  name,
226
232
  arguments: displayInput,
227
233
  text: displayInput !== input ? summarizeToolInput(name, displayInput || {}) : descText,
228
- });
234
+ ...(evolCommand ? { evolCommand } : {}),
235
+ ...(assistantUuid ? { forkAnchor: { assistantMessageId: assistantUuid } } : {}),
236
+ };
237
+ this.recordActivity(item);
238
+ if (this.suppressActivityItems())
239
+ return;
240
+ this.itemsQueue.push(item);
229
241
  this.messageTimestamps.push(Date.now());
230
242
  this.scheduleFlush();
231
243
  }
232
244
  /** 添加工具结果 */
233
- addToolResult(name, ok, result, error, callId, durationMs, descText, classification) {
245
+ addToolResult(name, ok, result, error, callId, durationMs, descText, classification, assistantUuid) {
234
246
  if (this.opts.envelope.chatmode === 'proactive')
235
247
  return;
236
248
  this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
237
- if (this.suppressActivityItems())
238
- return;
239
- this.itemsQueue.push({
249
+ const normalizedClassification = !ok
250
+ ? normalizeToolErrorClassification(classification ?? {})
251
+ : undefined;
252
+ const item = {
240
253
  kind: 'tool_result',
241
254
  call_id: callId || this.synthCallId(),
242
255
  name,
243
256
  ok,
244
257
  ...(result !== undefined && { result }),
245
258
  ...(error !== undefined && { error }),
246
- ...(!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
+ }),
247
268
  ...(durationMs !== undefined && { duration_ms: durationMs }),
248
269
  ...(descText !== undefined && { text: descText }),
249
- });
270
+ ...(assistantUuid ? { forkAnchor: { assistantMessageId: assistantUuid } } : {}),
271
+ };
272
+ this.recordActivity(item);
273
+ if (this.suppressActivityItems())
274
+ return;
275
+ this.itemsQueue.push(item);
250
276
  this.messageTimestamps.push(Date.now());
251
277
  this.scheduleFlush();
252
278
  }
@@ -318,6 +344,7 @@ export class IMRenderer {
318
344
  if (this.suppressActivityItems())
319
345
  return;
320
346
  this.itemsQueue.push(item);
347
+ this.recordActivity(item);
321
348
  this.messageTimestamps.push(Date.now());
322
349
  this.scheduleFlush();
323
350
  }
@@ -345,7 +372,9 @@ export class IMRenderer {
345
372
  }
346
373
  if (this.suppressActivityItems() && !force)
347
374
  return;
348
- 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);
349
378
  this.messageTimestamps.push(Date.now());
350
379
  this.scheduleFlush();
351
380
  }
@@ -498,7 +527,12 @@ export class IMRenderer {
498
527
  this.itemsQueue = this.itemsQueue.filter(it => it.kind === 'text');
499
528
  if (this.suppressActivityItems())
500
529
  return;
501
- 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 };
502
536
  this.sentContent = true;
503
537
  try {
504
538
  await this.sendPayload(payload);
@@ -686,8 +720,25 @@ export class IMRenderer {
686
720
  this.emitProactiveItem(item);
687
721
  }
688
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
+ }
689
736
  emitProactiveItem(item) {
690
- 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] };
691
742
  this.opts.send(payload).catch(err => {
692
743
  logger.debug(`[IMRenderer] proactive send failed: ${err.message}`);
693
744
  });
@@ -712,12 +763,14 @@ export class IMRenderer {
712
763
  }
713
764
  case 'tool_result':
714
765
  if (event.isError) {
766
+ const classification = normalizeToolErrorClassification(event);
715
767
  return {
716
768
  kind: 'tool_result',
717
769
  call_id: event.callId || this.synthCallId(),
718
770
  name: event.name,
719
771
  ok: false,
720
- error_code: classifyAunToolErrorCode(event),
772
+ error_code: classifyAunToolErrorCode({ ...event, ...classification }),
773
+ ...(classification.errorDetailCode ? { detail_code: classification.errorDetailCode } : {}),
721
774
  error: event.error || (typeof event.result === 'string' ? event.result : '执行失败'),
722
775
  ...(event.durationMs !== undefined && { duration_ms: event.durationMs }),
723
776
  };