evolclaw 2.8.3 → 3.0.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 (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -2,32 +2,55 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import crypto from 'crypto';
4
4
  import { hasCompact } from '../../agents/claude-runner.js';
5
- import { StreamFlusher } from './stream-flusher.js';
6
- import { ThoughtEmitter } from './thought-emitter.js';
5
+ import { appendMessageLog, buildOutboundEntry } from './message-log.js';
6
+ import { IMRenderer } from './im-renderer.js';
7
7
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
8
8
  import { logger } from '../../utils/logger.js';
9
9
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
10
10
  import { summarizeToolInput } from '../permission.js';
11
11
  import { DEFAULT_PERMISSION_MODE } from '../../types.js';
12
- import { getOwner } from '../../config.js';
13
12
  import { getPackageRoot, resolveRoot } from '../../paths.js';
14
13
  import { renderPromptSection } from '../../agents/templates.js';
14
+ import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
+ /**
16
+ * 构造 OutboundEnvelope —— 出站三件套的信封部分。
17
+ *
18
+ * 用于所有走 adapter.send 的出站路径:
19
+ * - 任务流内的 IMRenderer 投影(chatmode 由会话决定)
20
+ * - 命令回显(MessageBridge.handleCommand,taskId 用合成 ID `cmd-...`)
21
+ * - 网关层系统通知(src/index.ts,taskId 用 `system-...` / `restart-...` 等便于 events.log 关联)
22
+ *
23
+ * 注意:
24
+ * - chatmode 缺省 `'interactive'`(系统通知 / 命令回显都属于同步交互);
25
+ * - timestamp 可由调用方注入(便于测试),缺省 `Date.now()`。
26
+ */
27
+ export function buildEnvelope(opts) {
28
+ return {
29
+ taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
30
+ channel: opts.channel,
31
+ channelId: opts.channelId,
32
+ agentName: opts.agentName ?? '<unknown>',
33
+ chatmode: opts.chatmode ?? 'interactive',
34
+ replyContext: opts.replyContext,
35
+ timestamp: opts.timestamp ?? Date.now(),
36
+ };
37
+ }
15
38
  /**
16
39
  * 统一消息处理器
17
40
  * 负责处理来自不同渠道的消息,协调事件流处理
18
41
  */
19
42
  export class MessageProcessor {
20
43
  sessionManager;
21
- config;
44
+ globalSettings;
22
45
  messageCache;
23
46
  eventBus;
24
47
  commandHandler;
25
48
  channels = new Map();
26
49
  channelTypeMap = new Map(); // channelType → channelName(首个实例)
27
- currentFlusher;
50
+ currentRenderer;
28
51
  shouldSuppressActivities = false;
29
52
  agentMap;
30
- defaultAgentId;
53
+ primaryRunnerKey;
31
54
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
32
55
  interactionRouter;
33
56
  messageQueue;
@@ -38,18 +61,18 @@ export class MessageProcessor {
38
61
  * - `channel` is used to look up the owning EvolAgent (via registry).
39
62
  * - `baseagent` (e.g. 'claude') comes from `session.agentId`.
40
63
  *
41
- * Falls back to `defaultAgentId` (a composite key, e.g. `[default]::claude`)
64
+ * Falls back to `primaryRunnerKey` (a composite key, e.g. `aid::claude`)
42
65
  * when no match is found.
43
66
  */
44
67
  getAgent(channel, baseagent) {
45
68
  if (channel && baseagent) {
46
- const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
69
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
47
70
  const key = `${evolName}::${baseagent}`;
48
71
  if (this.agentMap.has(key))
49
72
  return this.agentMap.get(key);
50
73
  }
51
- if (this.agentMap.has(this.defaultAgentId))
52
- return this.agentMap.get(this.defaultAgentId);
74
+ if (this.agentMap.has(this.primaryRunnerKey))
75
+ return this.agentMap.get(this.primaryRunnerKey);
53
76
  return this.agentMap.values().next().value;
54
77
  }
55
78
  /** 获取可用 agent 列表 */
@@ -63,23 +86,23 @@ export class MessageProcessor {
63
86
  const active = await this.sessionManager.getActiveSession(channel, channelId);
64
87
  return active ? session.id !== active.id : false;
65
88
  }
66
- constructor(agentRunnerOrMap, sessionManager, config, messageCache, eventBus, commandHandler, defaultAgentId) {
89
+ constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
67
90
  this.sessionManager = sessionManager;
68
- this.config = config;
91
+ this.globalSettings = globalSettings;
69
92
  this.messageCache = messageCache;
70
93
  this.eventBus = eventBus;
71
94
  this.commandHandler = commandHandler;
72
95
  if (agentRunnerOrMap instanceof Map) {
73
96
  this.agentMap = agentRunnerOrMap;
74
- this.defaultAgentId = defaultAgentId || '[default]::claude';
97
+ this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
75
98
  }
76
99
  else {
77
- // Backward-compat single-runner path.
78
- this.agentMap = new Map([[`[default]::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
79
- this.defaultAgentId = `[default]::${agentRunnerOrMap.name}`;
100
+ // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
101
+ this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
102
+ this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
80
103
  }
81
104
  // 监听中断事件,标记被中断的 session
82
- this.eventBus.subscribe('message:interrupted', (event) => {
105
+ this.eventBus.subscribe('task:interrupted', (event) => {
83
106
  if ('sessionId' in event && event.sessionId) {
84
107
  this.interruptedSessions.set(event.sessionId, event.reason || 'unknown');
85
108
  }
@@ -101,7 +124,7 @@ export class MessageProcessor {
101
124
  const agent = this.agentRegistry.resolveByChannel(channelName);
102
125
  if (!agent)
103
126
  return null;
104
- const globalCm = this.config.chatmode;
127
+ const globalCm = this.agentRegistry?.resolveByChannel(channelName)?.config?.chatmode;
105
128
  return agent.getContext(channelName, chatType, globalCm);
106
129
  }
107
130
  /**
@@ -132,10 +155,10 @@ export class MessageProcessor {
132
155
  */
133
156
  handleCompactStart(sessionId) {
134
157
  if (sessionId) {
135
- this.eventBus.publish({ type: 'agent:compact-start', sessionId });
158
+ this.eventBus.publish({ type: 'runner:compact-start', sessionId });
136
159
  }
137
- if (this.currentFlusher && !this.shouldSuppressActivities) {
138
- this.currentFlusher.addActivity('\u23f3 会话压缩中...');
160
+ if (this.currentRenderer && !this.shouldSuppressActivities) {
161
+ this.currentRenderer.addNotice('\u23f3 会话压缩中...', 'info', 'compact-start', true);
139
162
  }
140
163
  }
141
164
  /**
@@ -170,7 +193,7 @@ export class MessageProcessor {
170
193
  * 处理消息(主入口)
171
194
  */
172
195
  async processMessage(message) {
173
- const idleMs = (this.config.idleMonitor?.timeout ?? 120) * 1000;
196
+ const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
174
197
  // 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
175
198
  // message.channel 现在存实例名(channelName),可直接用于精确路由
176
199
  const { session, absoluteProjectPath } = await this.resolveSession(message);
@@ -184,6 +207,7 @@ export class MessageProcessor {
184
207
  const streamKey = session.id;
185
208
  const chatType = message.chatType || 'private';
186
209
  const identityRole = session.identity?.role || 'anonymous';
210
+ const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
187
211
  // Resolve agent context from registry (Phase 2 foundation)
188
212
  const agentContext = this.getAgentContext(channelKey, chatType);
189
213
  if (agentContext) {
@@ -191,7 +215,7 @@ export class MessageProcessor {
191
215
  }
192
216
  // 按 session.agentId 选择 agent 后端
193
217
  const agent = this.getAgent(channelKey, session.agentId);
194
- const monitorEnabled = this.config.idleMonitor?.enabled !== false;
218
+ const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
195
219
  const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
196
220
  // 计算是否抑制中间输出(工具活动 + 流式文本)
197
221
  const shouldSuppress = () => {
@@ -217,13 +241,13 @@ export class MessageProcessor {
217
241
  while (result) {
218
242
  if (result.action === 'kill') {
219
243
  logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
220
- this.eventBus.publish({ type: 'agent:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
244
+ this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
221
245
  // 后台任务也需要中断(释放资源),但不发送通知
222
246
  if (channelInfo && !isBackground) {
223
247
  const msg = showIdleMonitor
224
248
  ? result.message
225
249
  : `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
226
- channelInfo.adapter.sendText(message.channelId, msg, this.getReplyContext(message)).catch(e => {
250
+ channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: msg, subtype: 'health' }).catch(e => {
227
251
  logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
228
252
  });
229
253
  }
@@ -239,7 +263,7 @@ export class MessageProcessor {
239
263
  logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
240
264
  if (channelInfo && showIdleMonitor && !shouldSuppress()) {
241
265
  if (!isBackground) {
242
- channelInfo.adapter.sendText(message.channelId, result.message, this.getReplyContext(message)).catch(e => {
266
+ channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: result.message, subtype: 'health' }).catch(e => {
243
267
  logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
244
268
  });
245
269
  }
@@ -302,8 +326,8 @@ export class MessageProcessor {
302
326
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
303
327
  const channelKey = session.metadata?.channelName || message.channel;
304
328
  const channelInfo = this.resolveChannelInfo(channelKey);
305
- // Per-method agent name for stats bucketing (agent.name or '[default]')
306
- const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '[default]';
329
+ // Per-method agent name for stats bucketing (agent.name or '<unknown>')
330
+ const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
307
331
  if (!channelInfo) {
308
332
  logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
309
333
  return;
@@ -329,8 +353,16 @@ export class MessageProcessor {
329
353
  metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
330
354
  };
331
355
  };
332
- // Proactive 模式可观测:ThoughtEmitter 声明在 try 外,catch 块也能透传错误为 thought
333
- let thoughtEmitter = null;
356
+ const isProactive = session.sessionMode === 'proactive';
357
+ const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
358
+ const envelope = buildEnvelope({
359
+ taskId,
360
+ channel: message.channel,
361
+ channelId: message.channelId,
362
+ agentName: agentNameForStats,
363
+ chatmode: isProactive ? 'proactive' : 'interactive',
364
+ replyContext: taskReplyContext(),
365
+ });
334
366
  try {
335
367
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
336
368
  // 记录收到消息
@@ -360,8 +392,11 @@ export class MessageProcessor {
360
392
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
361
393
  logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
362
394
  // 记录开始处理
363
- this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
364
- adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, taskReplyContext());
395
+ this.eventBus.publish({ type: 'task:started', sessionId: session.id });
396
+ // 触发器消息不发 processing status(无需通知用户)
397
+ if (message.source !== 'trigger') {
398
+ adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
399
+ }
365
400
  logger.message({
366
401
  msgId: messageId,
367
402
  sessionId: session.id,
@@ -369,42 +404,43 @@ export class MessageProcessor {
369
404
  status: 'processing'
370
405
  });
371
406
  const startTime = Date.now();
372
- // 创建 StreamFlusher,传入文件标记模式用于自动过滤
373
- // 使用动态判断,确保切换项目后不会继续输出
407
+ // 创建 IMRenderer(统一 interactive/proactive 两条路径)
374
408
  let firstReply = true;
375
- const isProactive = session.sessionMode === 'proactive';
376
- const flusher = new StreamFlusher(async (text, isFinal, hasText) => {
377
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
378
- if (!isCurrentlyBackground) {
409
+ const renderer = new IMRenderer({
410
+ adapter,
411
+ envelope,
412
+ flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
413
+ suppressActivities: shouldSuppress() || isAutonomous,
414
+ fileMarkerPattern: options?.fileMarkerPattern,
415
+ diagEnabled: this.globalSettings.debug?.flusherDiag,
416
+ send: async (payload) => {
417
+ if (isAutonomous)
418
+ return; // autonomous session: never send to channel
419
+ const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
420
+ if (isCurrentlyBackground)
421
+ return;
379
422
  const opts = {};
380
- if (isFinal)
381
- opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
382
- // replyContext 跟着任务走:优先用当前 message 的,兜底用 session 的(话题会话创建时写入)
383
- const replyCtx = this.getReplyContext(message);
384
- if (replyCtx) {
385
- Object.assign(opts, replyCtx);
423
+ const baseReplyCtx = this.getReplyContext(message);
424
+ if (baseReplyCtx) {
425
+ Object.assign(opts, baseReplyCtx);
386
426
  }
387
427
  else if (firstReply && message.messageId) {
388
- // 主会话:首条消息引用回复用户原消息(只在含真实文字时消费)
389
- if (hasText) {
428
+ if (payload.kind === 'result.text' && payload.text) {
390
429
  opts.replyToMessageId = message.messageId;
391
430
  firstReply = false;
392
431
  }
393
432
  }
433
+ if (payload.kind === 'result.text' && payload.isFinal) {
434
+ opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
435
+ }
394
436
  opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
395
- await adapter.sendText(message.channelId, text, opts);
396
- }
397
- // 后台任务:静默,不发送输出
398
- }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
399
- // 保存当前 flusher,用于 compact 事件
400
- this.currentFlusher = flusher;
437
+ const enrichedEnvelope = { ...envelope, replyContext: opts };
438
+ await adapter.send(enrichedEnvelope, payload);
439
+ },
440
+ });
441
+ this.currentRenderer = renderer;
401
442
  if (isProactive) {
402
- logger.info(`[MessageProcessor] proactive mode: flusher silent, outputs via thought.put task=${taskId}`);
403
- }
404
- // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
405
- // selector: context = { type: 'task', id: taskId }
406
- if (isProactive && adapter.putThought) {
407
- thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode, this.getReplyContext(message));
443
+ logger.info(`[MessageProcessor] proactive mode: outputs via thought.put task=${taskId}`);
408
444
  }
409
445
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
410
446
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
@@ -412,7 +448,7 @@ export class MessageProcessor {
412
448
  const capturedReplyContext = taskReplyContext();
413
449
  // 设置权限审批的消息发送回调(指向当前渠道)
414
450
  agent.setSendPrompt(async (text) => {
415
- await adapter.sendText(capturedChannelId, text, capturedReplyContext);
451
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: true });
416
452
  });
417
453
  // 设置权限审批的交互上下文(支持交互卡片)
418
454
  agent.setPermissionContext?.(session.id, {
@@ -420,6 +456,11 @@ export class MessageProcessor {
420
456
  channelId: capturedChannelId,
421
457
  replyContext: capturedReplyContext,
422
458
  interactionRouter: this.interactionRouter,
459
+ userId: message.peerId || undefined,
460
+ channel: message.channel,
461
+ agentName: agentNameForStats,
462
+ taskId,
463
+ chatmode: isProactive ? 'proactive' : 'interactive',
423
464
  interceptNextMessage: this.messageQueue
424
465
  ? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
425
466
  : undefined,
@@ -442,6 +483,7 @@ export class MessageProcessor {
442
483
  ? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
443
484
  : message.content;
444
485
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
486
+ let effectiveSystemPrompt;
445
487
  try {
446
488
  // 动态构建运行时上下文提示
447
489
  const contextParts = [];
@@ -465,9 +507,9 @@ export class MessageProcessor {
465
507
  let currentCanSend = false;
466
508
  if (!isProactive) {
467
509
  const fileChannelTypes = new Set();
468
- currentCanSend = !!channelInfo.adapter.sendFile;
510
+ currentCanSend = !!(channelInfo.adapter.capabilities?.file);
469
511
  for (const [, info] of this.channels) {
470
- if (info.adapter.sendFile) {
512
+ if (info.adapter.capabilities?.file) {
471
513
  fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
472
514
  }
473
515
  }
@@ -477,10 +519,20 @@ export class MessageProcessor {
477
519
  const capParts = [];
478
520
  if (options?.supportsImages)
479
521
  capParts.push('图片输入');
480
- if (channelInfo.adapter.sendImage)
522
+ if (channelInfo.adapter.capabilities?.image)
481
523
  capParts.push('图片输出');
482
- if (channelInfo.adapter.sendFile)
524
+ if (channelInfo.adapter.capabilities?.file)
483
525
  capParts.push('文件发送');
526
+ // Personal layer: persona.md + working memory 注入
527
+ const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
528
+ if (owningAgent) {
529
+ const persona = owningAgent.getPersona?.();
530
+ if (persona)
531
+ contextParts.push(persona);
532
+ const working = owningAgent.getWorkingMemory?.();
533
+ if (working)
534
+ contextParts.push(`[当前关注]\n${working}`);
535
+ }
484
536
  contextParts.push(renderPromptSection('runtime', {
485
537
  channel: currentChannelType,
486
538
  project: path.basename(absoluteProjectPath),
@@ -508,7 +560,13 @@ export class MessageProcessor {
508
560
  if (isProactive) {
509
561
  contextParts.push(renderPromptSection('proactive', {}));
510
562
  }
511
- const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
563
+ // 4. 触发器功能提示词(非触发器消息时注入,让 AI 知道可以使用触发器)
564
+ if (message.source !== 'trigger') {
565
+ const triggerSection = renderPromptSection('trigger', {});
566
+ if (triggerSection)
567
+ contextParts.push(triggerSection);
568
+ }
569
+ effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
512
570
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
513
571
  const MAX_RETRIES = 3;
514
572
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
@@ -518,7 +576,7 @@ export class MessageProcessor {
518
576
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
519
577
  agent.registerStream(streamKey, stream);
520
578
  streamRegistered = true;
521
- streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
579
+ streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
522
580
  break; // 成功,跳出重试循环
523
581
  }
524
582
  catch (retryError) {
@@ -528,8 +586,8 @@ export class MessageProcessor {
528
586
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
529
587
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
530
588
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
531
- flusher.addActivity(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`);
532
- await flusher.flush();
589
+ renderer.addNotice(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
590
+ await renderer.flush();
533
591
  await new Promise(resolve => setTimeout(resolve, delay));
534
592
  continue;
535
593
  }
@@ -540,15 +598,15 @@ export class MessageProcessor {
540
598
  catch (error) {
541
599
  if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
542
600
  // 尝试 compact 压缩会话
543
- flusher.addActivity('\u26a0\ufe0f 上下文过长,正在压缩会话...');
544
- await flusher.flush();
601
+ renderer.addNotice('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
602
+ await renderer.flush();
545
603
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
546
604
  if (compacted) {
547
605
  // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
548
- flusher.addActivity('\u2705 压缩完成,正在重试...');
549
- const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
606
+ renderer.addNotice('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
607
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
550
608
  agent.registerStream(streamKey, retryStream);
551
- streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
609
+ streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
552
610
  }
553
611
  else {
554
612
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -560,12 +618,12 @@ export class MessageProcessor {
560
618
  }
561
619
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
562
620
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
563
- // suppressed 模式下 flusher 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
621
+ // suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
564
622
  // proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
565
623
  if (!isProactive) {
566
624
  const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
567
625
  const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
568
- const flusherText = flusher.getFinalText();
626
+ const flusherText = renderer.getFinalText();
569
627
  const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
570
628
  const fileMatches = [...fullText.matchAll(markerPattern)];
571
629
  for (const match of fileMatches) {
@@ -596,22 +654,22 @@ export class MessageProcessor {
596
654
  && targetSpec !== currentChannelType;
597
655
  // 跨通道仅限 owner
598
656
  if (isCrossChannel && session.identity?.role !== 'owner') {
599
- await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, taskReplyContext());
657
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 跨通道发送仅限管理员`, subtype: 'fatal' });
600
658
  continue;
601
659
  }
602
660
  const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
603
661
  if (!fs.existsSync(resolvedPath)) {
604
662
  logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
605
- await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, taskReplyContext());
663
+ await adapter.send(envelope, { kind: 'system.error', text: `\u26a0\ufe0f 文件未找到: ${filePath}`, subtype: 'fatal' });
606
664
  continue;
607
665
  }
608
666
  // 找目标 adapter
609
667
  if (!targetInfo) {
610
- await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, taskReplyContext());
668
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 未启用或不存在`, subtype: 'channel_down' });
611
669
  continue;
612
670
  }
613
- if (!targetInfo.adapter.sendFile) {
614
- await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, taskReplyContext());
671
+ if (!targetInfo.adapter.capabilities?.file) {
672
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 不支持文件发送`, subtype: 'capability' });
615
673
  continue;
616
674
  }
617
675
  // 找目标 channelId
@@ -619,58 +677,58 @@ export class MessageProcessor {
619
677
  if (isCrossChannel) {
620
678
  const targetAdapterName = targetInfo.adapter.channelName;
621
679
  const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
622
- const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName) ?? getOwner(this.config, targetAdapterName);
680
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName);
623
681
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
624
682
  if (!targetChannelId) {
625
- await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
683
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, subtype: 'channel_down' });
626
684
  continue;
627
685
  }
628
686
  }
629
687
  logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
630
688
  try {
631
- await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, taskReplyContext());
632
- this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
689
+ await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: taskReplyContext() }), { kind: 'result.file', filePath: resolvedPath });
690
+ this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
633
691
  if (isCrossChannel) {
634
- await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, taskReplyContext());
692
+ await adapter.send(envelope, { kind: 'system.notice', text: `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, subtype: 'health' });
635
693
  }
636
694
  }
637
695
  catch (error) {
638
696
  logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
639
- await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, taskReplyContext());
697
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 文件发送失败: ${filePath}`, subtype: 'fatal' });
640
698
  }
641
699
  }
642
700
  } // end of !isProactive
643
- // 最终回复文本添加到 flusher(统一在流结束后处理,避免多 complete 事件重复发送)
701
+ // 最终回复文本添加到 renderer(统一在流结束后处理,避免多 complete 事件重复发送)
644
702
  // suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
645
703
  // 非 suppressed 且无流式文本:同上
646
704
  // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
647
- // 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
705
+ // 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
648
706
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
649
707
  // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
650
708
  // 特征:无流式 text + complete.result 匹配已知模式
651
709
  // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
652
- // Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
710
+ // Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
653
711
  const isSdkFallbackMessage = !!finalReplyText
654
712
  && !streamResult.hasReceivedText
655
713
  && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
656
714
  if (finalReplyText) {
657
715
  if (isProactive && isSdkFallbackMessage) {
658
- // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
716
+ // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent renderer
659
717
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
660
718
  if (!isCurrentlyBackground) {
661
- await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
719
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
662
720
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
663
721
  }
664
722
  }
665
723
  else if (shouldSuppress()) {
666
- flusher.addText(finalReplyText);
724
+ renderer.addText(finalReplyText);
667
725
  }
668
- else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
669
- flusher.addText(finalReplyText);
726
+ else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
727
+ renderer.addText(finalReplyText);
670
728
  }
671
729
  }
672
730
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
673
- await flusher.flush(true);
731
+ await renderer.flush(true);
674
732
  // 清理 activeStreams(正常完成)
675
733
  agent.cleanupStream(streamKey);
676
734
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
@@ -690,9 +748,14 @@ export class MessageProcessor {
690
748
  const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
691
749
  const rawSubtype = streamResult.subtype || 'agent_error';
692
750
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
693
- adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, taskReplyContext());
751
+ if (message.source !== 'trigger') {
752
+ adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
753
+ }
754
+ if (message.triggerMeta) {
755
+ this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, messageId: messageId, error: errorSummary });
756
+ }
694
757
  this.eventBus.publish({
695
- type: 'message:error',
758
+ type: 'task:error',
696
759
  sessionId: session.id,
697
760
  error: errorSummary,
698
761
  errorType,
@@ -719,10 +782,30 @@ export class MessageProcessor {
719
782
  }
720
783
  else {
721
784
  // 真正的成功
722
- adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, taskReplyContext());
785
+ const durationMs = Date.now() - startTime;
786
+ if (message.source !== 'trigger') {
787
+ if (interruptReason) {
788
+ adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
789
+ }
790
+ else {
791
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs } }).catch(() => { });
792
+ }
793
+ }
794
+ if (message.triggerMeta) {
795
+ if (interruptReason) {
796
+ this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, reason: 'interrupted' });
797
+ }
798
+ else {
799
+ this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
800
+ }
801
+ // Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
802
+ if (session.sessionMode === 'autonomous') {
803
+ this.sessionManager.unbindSession(session.id).catch(() => { });
804
+ }
805
+ }
723
806
  await this.sessionManager.recordSuccess(session.id);
724
807
  this.eventBus.publish({
725
- type: 'message:completed',
808
+ type: 'task:completed',
726
809
  sessionId: session.id,
727
810
  channel: message.channel,
728
811
  channelId: message.channelId,
@@ -740,12 +823,28 @@ export class MessageProcessor {
740
823
  status: 'completed',
741
824
  duration: Date.now() - startTime
742
825
  });
826
+ // 写入消息记录(出方向)
827
+ if (streamResult.lastReplyText || streamResult.fullText) {
828
+ const chatDir = this.sessionManager.getChatDir(session);
829
+ appendMessageLog(chatDir, buildOutboundEntry({
830
+ from: message.selfId || session.selfId || 'self',
831
+ to: message.peerId || message.channelId,
832
+ chatType: (message.chatType || session.chatType || 'private'),
833
+ groupId: session.metadata?.groupId ?? null,
834
+ msgId: `${messageId}_reply`,
835
+ content: streamResult.lastReplyText || streamResult.fullText,
836
+ replyTo: message.messageId ?? null,
837
+ agent: session.agentId || null,
838
+ model: agent.getModel?.() || null,
839
+ durationMs: Date.now() - startTime,
840
+ }));
841
+ }
743
842
  }
744
843
  const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
745
- if (isFinallyBackground) {
844
+ if (isFinallyBackground && session.sessionMode !== 'autonomous') {
746
845
  const projectName = path.basename(session.projectPath);
747
846
  const count = this.messageCache.getCount(session.id);
748
- await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, taskReplyContext());
847
+ await adapter.send(envelope, { kind: 'system.notice', text: `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, subtype: 'background' });
749
848
  }
750
849
  // 记录发送响应
751
850
  logger.message({
@@ -774,10 +873,12 @@ export class MessageProcessor {
774
873
  : 'error';
775
874
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
776
875
  if (!isUserInterrupt) {
777
- try {
778
- adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, taskReplyContext());
779
- }
780
- catch { }
876
+ const statusPayload = procStatus === 'timeout'
877
+ ? { kind: 'status.timeout' }
878
+ : procStatus === 'interrupted'
879
+ ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
880
+ : { kind: 'status.error' };
881
+ adapter.send(envelope, statusPayload).catch(() => { });
781
882
  }
782
883
  // 用户主动中断时降级日志;其余仍按 error 记录
783
884
  if (isUserInterrupt) {
@@ -789,7 +890,7 @@ export class MessageProcessor {
789
890
  const errorMsg = error instanceof Error ? error.message : String(error);
790
891
  const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
791
892
  this.eventBus.publish({
792
- type: 'message:error',
893
+ type: 'task:error',
793
894
  sessionId: session.id,
794
895
  error: errorMsg,
795
896
  errorType,
@@ -808,7 +909,7 @@ export class MessageProcessor {
808
909
  }
809
910
  // 发送用户友好的错误消息(SDK_TIMEOUT 已在 kill 级别发过提示,跳过)
810
911
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
811
- // processEventStream 已通过 flusher 发过错误时也跳过
912
+ // processEventStream 已通过 renderer 发过错误时也跳过
812
913
  if (error instanceof Error && error.message === 'SDK_TIMEOUT') {
813
914
  logger.info(`[MessageProcessor] SDK_TIMEOUT error, skip sending duplicate message`);
814
915
  }
@@ -816,14 +917,14 @@ export class MessageProcessor {
816
917
  logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
817
918
  }
818
919
  else if (error?._errorAlreadySent) {
819
- logger.info(`[MessageProcessor] Error already sent via flusher, skip sending duplicate message`);
920
+ logger.info(`[MessageProcessor] Error already sent via renderer, skip sending duplicate message`);
820
921
  }
821
922
  else {
822
923
  const userMessage = getErrorMessage(error, undefined);
823
924
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
824
925
  let sendOpts;
825
926
  try {
826
- await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
927
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId);
827
928
  sendOpts = this.getReplyContext(message);
828
929
  }
829
930
  catch { }
@@ -832,15 +933,8 @@ export class MessageProcessor {
832
933
  ...(sendOpts ?? {}),
833
934
  metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
834
935
  };
835
- await adapter.sendText(message.channelId, userMessage, sendOpts);
936
+ await adapter.send({ ...envelope, replyContext: sendOpts }, { kind: 'result.text', text: userMessage, isFinal: true });
836
937
  // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
837
- if (thoughtEmitter) {
838
- const thoughtErrorType = errType === ErrorType.CONTEXT_TOO_LONG ? 'context_too_long' :
839
- errType === ErrorType.AUTH_ERROR ? 'auth' :
840
- (errType === ErrorType.SDK_TIMEOUT || errType === ErrorType.STREAM_ERROR) ? 'network' :
841
- 'unknown';
842
- thoughtEmitter.emit({ type: 'error', error: userMessage, errorType: thoughtErrorType }).catch(() => { });
843
- }
844
938
  }
845
939
  }
846
940
  }
@@ -852,7 +946,22 @@ export class MessageProcessor {
852
946
  const metadata = (message.threadId && message.replyContext)
853
947
  ? { replyContext: message.replyContext }
854
948
  : undefined;
855
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata, undefined, message.peerId);
949
+ const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
950
+ // --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
951
+ if (message.triggerMeta?.silent) {
952
+ const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
953
+ const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
954
+ await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
955
+ session.sessionMode = 'autonomous';
956
+ if (prevActive) {
957
+ await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
958
+ }
959
+ const absoluteProjectPath = path.isAbsolute(session.projectPath)
960
+ ? session.projectPath
961
+ : path.resolve(process.cwd(), session.projectPath);
962
+ return { session, absoluteProjectPath };
963
+ }
964
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId);
856
965
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
857
966
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
858
967
  ? session.projectPath
@@ -865,9 +974,9 @@ export class MessageProcessor {
865
974
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
866
975
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
867
976
  */
868
- async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
977
+ async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
869
978
  // Per-session agent name for stats bucketing
870
- const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '[default]';
979
+ const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
871
980
  let hasReceivedText = false;
872
981
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
873
982
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -906,10 +1015,8 @@ export class MessageProcessor {
906
1015
  else {
907
1016
  logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
908
1017
  }
909
- // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
910
- if (thoughtEmitter) {
911
- thoughtEmitter.emit(event).catch(() => { });
912
- }
1018
+ // IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
1019
+ renderer.emit(event);
913
1020
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
914
1021
  if (event.type === 'session_id') {
915
1022
  logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
@@ -918,14 +1025,14 @@ export class MessageProcessor {
918
1025
  // session 状态变更(idle/running/requires_action)
919
1026
  if (event.type === 'state_changed') {
920
1027
  logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
921
- this.eventBus.publish({ type: 'agent:state-changed', sessionId: session.id, state: event.state });
1028
+ this.eventBus.publish({ type: 'runner:state-changed', sessionId: session.id, state: event.state });
922
1029
  continue;
923
1030
  }
924
1031
  // agent 状态通知(仅事件,不直出给用户)
925
1032
  if (event.type === 'status') {
926
1033
  logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
927
1034
  this.eventBus.publish({
928
- type: 'agent:status',
1035
+ type: 'runner:status',
929
1036
  sessionId: session.id,
930
1037
  subtype: event.subtype,
931
1038
  message: event.message,
@@ -942,14 +1049,14 @@ export class MessageProcessor {
942
1049
  lastReplyText += event.text;
943
1050
  this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
944
1051
  if (!shouldSuppress()) {
945
- flusher.addText(event.text);
1052
+ renderer.addText(event.text);
946
1053
  }
947
1054
  }
948
1055
  // compact 完成
949
1056
  if (event.type === 'compact') {
950
- this.eventBus.publish({ type: 'agent:compact-complete', sessionId: session.id, preTokens: event.preTokens });
1057
+ this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
951
1058
  if (!shouldSuppress()) {
952
- flusher.addActivity(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`);
1059
+ renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
953
1060
  }
954
1061
  }
955
1062
  // 子任务进度
@@ -958,10 +1065,10 @@ export class MessageProcessor {
958
1065
  const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
959
1066
  const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
960
1067
  if (event.summary && !shouldSuppress()) {
961
- flusher.addActivity(`\u23f3 \u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`);
1068
+ renderer.addProgress(`\u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
962
1069
  }
963
1070
  else if (stats && !shouldSuppress()) {
964
- flusher.addActivity(`\u23f3 \u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`);
1071
+ renderer.addProgress(`\u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
965
1072
  }
966
1073
  }
967
1074
  // 工具调用
@@ -977,7 +1084,7 @@ export class MessageProcessor {
977
1084
  });
978
1085
  if (!shouldSuppress()) {
979
1086
  const desc = summarizeToolInput(event.name, event.input || {});
980
- flusher.addActivity(`\ud83d\udd27 ${event.name}${desc ? ': ' + desc : ''}`);
1087
+ renderer.addToolCall(event.name, event.input, event.callId, desc);
981
1088
  }
982
1089
  }
983
1090
  // 工具结果
@@ -996,7 +1103,10 @@ export class MessageProcessor {
996
1103
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
997
1104
  // 移除 XML 风格的错误标签
998
1105
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
999
- flusher.addActivity(`\u26a0\ufe0f ${event.name || '\u5de5\u5177'}: ${errorMsg}`);
1106
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId);
1107
+ }
1108
+ else if (!event.isError && !shouldSuppress()) {
1109
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId);
1000
1110
  }
1001
1111
  }
1002
1112
  // 运行时错误(Codex: turn.failed / item error)
@@ -1004,7 +1114,7 @@ export class MessageProcessor {
1004
1114
  logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
1005
1115
  if (!hasErrorResult && !shouldSuppress()) {
1006
1116
  hasErrorResult = true;
1007
- flusher.addActivity(`\u274c ${event.error}`);
1117
+ renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
1008
1118
  }
1009
1119
  }
1010
1120
  // 完成事件
@@ -1029,12 +1139,12 @@ export class MessageProcessor {
1029
1139
  const userFriendlyMessage = event.terminalReason
1030
1140
  ? getErrorMessage(null, event.terminalReason)
1031
1141
  : `\u274c ${errorSummary}`;
1032
- flusher.addActivity(userFriendlyMessage);
1142
+ renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
1033
1143
  }
1034
1144
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
1035
1145
  // 最终文本留给流结束后的统一 flush(true)
1036
- if (flusher.hasContent()) {
1037
- await flusher.flushActivitiesOnly();
1146
+ if (renderer.hasContent()) {
1147
+ await renderer.flushActivitiesOnly();
1038
1148
  }
1039
1149
  }
1040
1150
  continue;
@@ -1068,7 +1178,7 @@ export class MessageProcessor {
1068
1178
  });
1069
1179
  // 后台任务完成也纳入统计
1070
1180
  this.eventBus.publish({
1071
- type: 'message:completed',
1181
+ type: 'task:completed',
1072
1182
  sessionId: session.id,
1073
1183
  channel: session.channel,
1074
1184
  channelId: session.channelId,
@@ -1090,7 +1200,7 @@ export class MessageProcessor {
1090
1200
  });
1091
1201
  // 后台任务失败也纳入统计
1092
1202
  this.eventBus.publish({
1093
- type: 'message:error',
1203
+ type: 'task:error',
1094
1204
  sessionId: session.id,
1095
1205
  error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
1096
1206
  errorType: bgErrorType,
@@ -1121,13 +1231,13 @@ export class MessageProcessor {
1121
1231
  logger.error('[MessageProcessor] Stream processing error:', error);
1122
1232
  }
1123
1233
  if (error instanceof Error && error.message.includes('process exited')) {
1124
- flusher.addActivity('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5');
1234
+ renderer.addNotice('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
1125
1235
  }
1126
1236
  // Flush any pending error activities before re-throwing,
1127
1237
  // and mark the error so outer catch won't send a duplicate message
1128
- if (hasErrorResult || flusher.hasContent()) {
1238
+ if (hasErrorResult || renderer.hasContent()) {
1129
1239
  try {
1130
- await flusher.flush(true);
1240
+ await renderer.flush(true);
1131
1241
  }
1132
1242
  catch { }
1133
1243
  if (error instanceof Error) {
@@ -1261,8 +1371,65 @@ export class MessageProcessor {
1261
1371
  if (/^[.\s\u2026]+$/.test(filePath))
1262
1372
  return true;
1263
1373
  // 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
1264
- if (/[\\[\]{}*+?|^$]/.test(filePath))
1374
+ if (/[\[\]{}*+?|^$]/.test(filePath))
1265
1375
  return true;
1266
1376
  return false;
1267
1377
  }
1268
1378
  }
1379
+ // ── 出站协议辅助:buildEnvelope / sendInteractionPayload ──
1380
+ // Phase 3 of outbound unification: callers (permission flow, CommandHandler
1381
+ // interaction cards, claude-runner AskUserQuestion / ExitPlanMode) should
1382
+ // produce `{ kind: 'interaction', interaction, fallbackText }` and dispatch
1383
+ // via `adapter.send(envelope, payload)` instead of calling
1384
+ // `adapter.sendInteraction(...)` directly. These helpers centralise the
1385
+ // indirection and provide a backwards-compatible fallback path for adapters
1386
+ // that do not yet implement `send`.
1387
+ /**
1388
+ * Default fallback text for an InteractionRequest. Used when the caller
1389
+ * does not supply one explicitly. Picks the appropriate renderer based on
1390
+ * the interaction kind.
1391
+ */
1392
+ export function defaultFallbackText(interaction) {
1393
+ const kind = interaction.kind;
1394
+ if (kind.kind === 'command-card') {
1395
+ return renderCommandCardAsText(kind);
1396
+ }
1397
+ if (kind.kind === 'action') {
1398
+ try {
1399
+ return renderActionAsText(interaction);
1400
+ }
1401
+ catch {
1402
+ // ActionInteraction without fallback metadata — produce a minimal hint
1403
+ const action = kind;
1404
+ const lines = [action.title];
1405
+ if (action.body)
1406
+ lines.push(action.body);
1407
+ return lines.join('\n');
1408
+ }
1409
+ }
1410
+ return '';
1411
+ }
1412
+ /**
1413
+ * Send an interaction payload through the unified `adapter.send` entrypoint.
1414
+ *
1415
+ * Sends an interaction via adapter.send(envelope, { kind: 'interaction', ... }).
1416
+ * Returns 'sent' on success, false on failure.
1417
+ */
1418
+ export async function sendInteractionPayload(adapter, envelope, interaction, fallbackText, replyCtx) {
1419
+ const text = fallbackText ?? defaultFallbackText(interaction);
1420
+ const payload = {
1421
+ kind: 'interaction',
1422
+ interaction,
1423
+ fallbackText: text || undefined,
1424
+ };
1425
+ try {
1426
+ const enriched = replyCtx
1427
+ ? { ...envelope, replyContext: replyCtx }
1428
+ : envelope;
1429
+ await adapter.send(enriched, payload);
1430
+ return 'sent';
1431
+ }
1432
+ catch {
1433
+ return false;
1434
+ }
1435
+ }