evolclaw 2.8.2 → 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 (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  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 +1064 -279
  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/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  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 +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  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 +437 -273
  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 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. package/dist/utils/stats-collector.js +0 -99
@@ -2,41 +2,78 @@ 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;
34
57
  skillsEnsured = false; // 全局 SKILLS.md 是否已确保
35
- /** 按 agentId 获取 agent,回退到默认 */
36
- getAgent(agentId) {
37
- if (agentId && this.agentMap.has(agentId))
38
- return this.agentMap.get(agentId);
39
- return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
58
+ /**
59
+ * Get the runner for a given (channel, baseagent) pair.
60
+ *
61
+ * - `channel` is used to look up the owning EvolAgent (via registry).
62
+ * - `baseagent` (e.g. 'claude') comes from `session.agentId`.
63
+ *
64
+ * Falls back to `primaryRunnerKey` (a composite key, e.g. `aid::claude`)
65
+ * when no match is found.
66
+ */
67
+ getAgent(channel, baseagent) {
68
+ if (channel && baseagent) {
69
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
70
+ const key = `${evolName}::${baseagent}`;
71
+ if (this.agentMap.has(key))
72
+ return this.agentMap.get(key);
73
+ }
74
+ if (this.agentMap.has(this.primaryRunnerKey))
75
+ return this.agentMap.get(this.primaryRunnerKey);
76
+ return this.agentMap.values().next().value;
40
77
  }
41
78
  /** 获取可用 agent 列表 */
42
79
  getAvailableAgents() {
@@ -49,23 +86,23 @@ export class MessageProcessor {
49
86
  const active = await this.sessionManager.getActiveSession(channel, channelId);
50
87
  return active ? session.id !== active.id : false;
51
88
  }
52
- constructor(agentRunnerOrMap, sessionManager, config, messageCache, eventBus, commandHandler, defaultAgentId) {
89
+ constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
53
90
  this.sessionManager = sessionManager;
54
- this.config = config;
91
+ this.globalSettings = globalSettings;
55
92
  this.messageCache = messageCache;
56
93
  this.eventBus = eventBus;
57
94
  this.commandHandler = commandHandler;
58
95
  if (agentRunnerOrMap instanceof Map) {
59
96
  this.agentMap = agentRunnerOrMap;
60
- this.defaultAgentId = defaultAgentId || 'claude';
97
+ this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
61
98
  }
62
99
  else {
63
- // 向后兼容:单个 agentRunner
64
- this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
65
- this.defaultAgentId = agentRunnerOrMap.name;
100
+ // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
101
+ this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
102
+ this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
66
103
  }
67
104
  // 监听中断事件,标记被中断的 session
68
- this.eventBus.subscribe('message:interrupted', (event) => {
105
+ this.eventBus.subscribe('task:interrupted', (event) => {
69
106
  if ('sessionId' in event && event.sessionId) {
70
107
  this.interruptedSessions.set(event.sessionId, event.reason || 'unknown');
71
108
  }
@@ -87,7 +124,7 @@ export class MessageProcessor {
87
124
  const agent = this.agentRegistry.resolveByChannel(channelName);
88
125
  if (!agent)
89
126
  return null;
90
- const globalCm = this.config.chatmode;
127
+ const globalCm = this.agentRegistry?.resolveByChannel(channelName)?.config?.chatmode;
91
128
  return agent.getContext(channelName, chatType, globalCm);
92
129
  }
93
130
  /**
@@ -118,10 +155,10 @@ export class MessageProcessor {
118
155
  */
119
156
  handleCompactStart(sessionId) {
120
157
  if (sessionId) {
121
- this.eventBus.publish({ type: 'agent:compact-start', sessionId });
158
+ this.eventBus.publish({ type: 'runner:compact-start', sessionId });
122
159
  }
123
- if (this.currentFlusher && !this.shouldSuppressActivities) {
124
- this.currentFlusher.addActivity('\u23f3 会话压缩中...');
160
+ if (this.currentRenderer && !this.shouldSuppressActivities) {
161
+ this.currentRenderer.addNotice('\u23f3 会话压缩中...', 'info', 'compact-start', true);
125
162
  }
126
163
  }
127
164
  /**
@@ -156,7 +193,7 @@ export class MessageProcessor {
156
193
  * 处理消息(主入口)
157
194
  */
158
195
  async processMessage(message) {
159
- const idleMs = (this.config.idleMonitor?.timeout ?? 120) * 1000;
196
+ const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
160
197
  // 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
161
198
  // message.channel 现在存实例名(channelName),可直接用于精确路由
162
199
  const { session, absoluteProjectPath } = await this.resolveSession(message);
@@ -170,14 +207,15 @@ export class MessageProcessor {
170
207
  const streamKey = session.id;
171
208
  const chatType = message.chatType || 'private';
172
209
  const identityRole = session.identity?.role || 'anonymous';
210
+ const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
173
211
  // Resolve agent context from registry (Phase 2 foundation)
174
212
  const agentContext = this.getAgentContext(channelKey, chatType);
175
213
  if (agentContext) {
176
214
  logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
177
215
  }
178
216
  // 按 session.agentId 选择 agent 后端
179
- const agent = this.getAgent(session.agentId);
180
- const monitorEnabled = this.config.idleMonitor?.enabled !== false;
217
+ const agent = this.getAgent(channelKey, session.agentId);
218
+ const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
181
219
  const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
182
220
  // 计算是否抑制中间输出(工具活动 + 流式文本)
183
221
  const shouldSuppress = () => {
@@ -203,13 +241,13 @@ export class MessageProcessor {
203
241
  while (result) {
204
242
  if (result.action === 'kill') {
205
243
  logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
206
- 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 });
207
245
  // 后台任务也需要中断(释放资源),但不发送通知
208
246
  if (channelInfo && !isBackground) {
209
247
  const msg = showIdleMonitor
210
248
  ? result.message
211
249
  : `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
212
- 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 => {
213
251
  logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
214
252
  });
215
253
  }
@@ -225,7 +263,7 @@ export class MessageProcessor {
225
263
  logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
226
264
  if (channelInfo && showIdleMonitor && !shouldSuppress()) {
227
265
  if (!isBackground) {
228
- 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 => {
229
267
  logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
230
268
  });
231
269
  }
@@ -288,8 +326,8 @@ export class MessageProcessor {
288
326
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
289
327
  const channelKey = session.metadata?.channelName || message.channel;
290
328
  const channelInfo = this.resolveChannelInfo(channelKey);
291
- // Per-method agent name for stats bucketing (agent.name or '[default]')
292
- 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>';
293
331
  if (!channelInfo) {
294
332
  logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
295
333
  return;
@@ -302,7 +340,7 @@ export class MessageProcessor {
302
340
  return;
303
341
  }
304
342
  const { adapter, options } = channelInfo;
305
- const agent = this.getAgent(session.agentId);
343
+ const agent = this.getAgent(channelKey, session.agentId);
306
344
  const streamKey = session.id;
307
345
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
308
346
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
@@ -315,8 +353,16 @@ export class MessageProcessor {
315
353
  metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
316
354
  };
317
355
  };
318
- // Proactive 模式可观测:ThoughtEmitter 声明在 try 外,catch 块也能透传错误为 thought
319
- 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
+ });
320
366
  try {
321
367
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
322
368
  // 记录收到消息
@@ -346,8 +392,11 @@ export class MessageProcessor {
346
392
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
347
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'}`);
348
394
  // 记录开始处理
349
- this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
350
- 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
+ }
351
400
  logger.message({
352
401
  msgId: messageId,
353
402
  sessionId: session.id,
@@ -355,42 +404,43 @@ export class MessageProcessor {
355
404
  status: 'processing'
356
405
  });
357
406
  const startTime = Date.now();
358
- // 创建 StreamFlusher,传入文件标记模式用于自动过滤
359
- // 使用动态判断,确保切换项目后不会继续输出
407
+ // 创建 IMRenderer(统一 interactive/proactive 两条路径)
360
408
  let firstReply = true;
361
- const isProactive = session.sessionMode === 'proactive';
362
- const flusher = new StreamFlusher(async (text, isFinal, hasText) => {
363
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
364
- 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;
365
422
  const opts = {};
366
- if (isFinal)
367
- opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
368
- // replyContext 跟着任务走:优先用当前 message 的,兜底用 session 的(话题会话创建时写入)
369
- const replyCtx = this.getReplyContext(message);
370
- if (replyCtx) {
371
- Object.assign(opts, replyCtx);
423
+ const baseReplyCtx = this.getReplyContext(message);
424
+ if (baseReplyCtx) {
425
+ Object.assign(opts, baseReplyCtx);
372
426
  }
373
427
  else if (firstReply && message.messageId) {
374
- // 主会话:首条消息引用回复用户原消息(只在含真实文字时消费)
375
- if (hasText) {
428
+ if (payload.kind === 'result.text' && payload.text) {
376
429
  opts.replyToMessageId = message.messageId;
377
430
  firstReply = false;
378
431
  }
379
432
  }
433
+ if (payload.kind === 'result.text' && payload.isFinal) {
434
+ opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
435
+ }
380
436
  opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
381
- await adapter.sendText(message.channelId, text, opts);
382
- }
383
- // 后台任务:静默,不发送输出
384
- }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
385
- // 保存当前 flusher,用于 compact 事件
386
- this.currentFlusher = flusher;
437
+ const enrichedEnvelope = { ...envelope, replyContext: opts };
438
+ await adapter.send(enrichedEnvelope, payload);
439
+ },
440
+ });
441
+ this.currentRenderer = renderer;
387
442
  if (isProactive) {
388
- logger.info(`[MessageProcessor] proactive mode: flusher silent, outputs via thought.put task=${taskId}`);
389
- }
390
- // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
391
- // selector: context = { type: 'task', id: taskId }
392
- if (isProactive && adapter.putThought) {
393
- thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode, this.getReplyContext(message));
443
+ logger.info(`[MessageProcessor] proactive mode: outputs via thought.put task=${taskId}`);
394
444
  }
395
445
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
396
446
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
@@ -398,7 +448,7 @@ export class MessageProcessor {
398
448
  const capturedReplyContext = taskReplyContext();
399
449
  // 设置权限审批的消息发送回调(指向当前渠道)
400
450
  agent.setSendPrompt(async (text) => {
401
- await adapter.sendText(capturedChannelId, text, capturedReplyContext);
451
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: true });
402
452
  });
403
453
  // 设置权限审批的交互上下文(支持交互卡片)
404
454
  agent.setPermissionContext?.(session.id, {
@@ -406,6 +456,11 @@ export class MessageProcessor {
406
456
  channelId: capturedChannelId,
407
457
  replyContext: capturedReplyContext,
408
458
  interactionRouter: this.interactionRouter,
459
+ userId: message.peerId || undefined,
460
+ channel: message.channel,
461
+ agentName: agentNameForStats,
462
+ taskId,
463
+ chatmode: isProactive ? 'proactive' : 'interactive',
409
464
  interceptNextMessage: this.messageQueue
410
465
  ? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
411
466
  : undefined,
@@ -428,6 +483,7 @@ export class MessageProcessor {
428
483
  ? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
429
484
  : message.content;
430
485
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
486
+ let effectiveSystemPrompt;
431
487
  try {
432
488
  // 动态构建运行时上下文提示
433
489
  const contextParts = [];
@@ -451,9 +507,9 @@ export class MessageProcessor {
451
507
  let currentCanSend = false;
452
508
  if (!isProactive) {
453
509
  const fileChannelTypes = new Set();
454
- currentCanSend = !!channelInfo.adapter.sendFile;
510
+ currentCanSend = !!(channelInfo.adapter.capabilities?.file);
455
511
  for (const [, info] of this.channels) {
456
- if (info.adapter.sendFile) {
512
+ if (info.adapter.capabilities?.file) {
457
513
  fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
458
514
  }
459
515
  }
@@ -463,10 +519,20 @@ export class MessageProcessor {
463
519
  const capParts = [];
464
520
  if (options?.supportsImages)
465
521
  capParts.push('图片输入');
466
- if (channelInfo.adapter.sendImage)
522
+ if (channelInfo.adapter.capabilities?.image)
467
523
  capParts.push('图片输出');
468
- if (channelInfo.adapter.sendFile)
524
+ if (channelInfo.adapter.capabilities?.file)
469
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
+ }
470
536
  contextParts.push(renderPromptSection('runtime', {
471
537
  channel: currentChannelType,
472
538
  project: path.basename(absoluteProjectPath),
@@ -494,7 +560,13 @@ export class MessageProcessor {
494
560
  if (isProactive) {
495
561
  contextParts.push(renderPromptSection('proactive', {}));
496
562
  }
497
- 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;
498
570
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
499
571
  const MAX_RETRIES = 3;
500
572
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
@@ -504,7 +576,7 @@ export class MessageProcessor {
504
576
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
505
577
  agent.registerStream(streamKey, stream);
506
578
  streamRegistered = true;
507
- streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
579
+ streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
508
580
  break; // 成功,跳出重试循环
509
581
  }
510
582
  catch (retryError) {
@@ -514,8 +586,8 @@ export class MessageProcessor {
514
586
  if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
515
587
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
516
588
  logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
517
- flusher.addActivity(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`);
518
- await flusher.flush();
589
+ renderer.addNotice(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
590
+ await renderer.flush();
519
591
  await new Promise(resolve => setTimeout(resolve, delay));
520
592
  continue;
521
593
  }
@@ -526,15 +598,15 @@ export class MessageProcessor {
526
598
  catch (error) {
527
599
  if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
528
600
  // 尝试 compact 压缩会话
529
- flusher.addActivity('\u26a0\ufe0f 上下文过长,正在压缩会话...');
530
- await flusher.flush();
601
+ renderer.addNotice('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
602
+ await renderer.flush();
531
603
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
532
604
  if (compacted) {
533
605
  // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
534
- flusher.addActivity('\u2705 压缩完成,正在重试...');
535
- 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);
536
608
  agent.registerStream(streamKey, retryStream);
537
- streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
609
+ streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
538
610
  }
539
611
  else {
540
612
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -546,12 +618,12 @@ export class MessageProcessor {
546
618
  }
547
619
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
548
620
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
549
- // suppressed 模式下 flusher 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
621
+ // suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
550
622
  // proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
551
623
  if (!isProactive) {
552
624
  const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
553
625
  const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
554
- const flusherText = flusher.getFinalText();
626
+ const flusherText = renderer.getFinalText();
555
627
  const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
556
628
  const fileMatches = [...fullText.matchAll(markerPattern)];
557
629
  for (const match of fileMatches) {
@@ -582,22 +654,22 @@ export class MessageProcessor {
582
654
  && targetSpec !== currentChannelType;
583
655
  // 跨通道仅限 owner
584
656
  if (isCrossChannel && session.identity?.role !== 'owner') {
585
- await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, taskReplyContext());
657
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 跨通道发送仅限管理员`, subtype: 'fatal' });
586
658
  continue;
587
659
  }
588
660
  const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
589
661
  if (!fs.existsSync(resolvedPath)) {
590
662
  logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
591
- await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, taskReplyContext());
663
+ await adapter.send(envelope, { kind: 'system.error', text: `\u26a0\ufe0f 文件未找到: ${filePath}`, subtype: 'fatal' });
592
664
  continue;
593
665
  }
594
666
  // 找目标 adapter
595
667
  if (!targetInfo) {
596
- await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, taskReplyContext());
668
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 未启用或不存在`, subtype: 'channel_down' });
597
669
  continue;
598
670
  }
599
- if (!targetInfo.adapter.sendFile) {
600
- 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' });
601
673
  continue;
602
674
  }
603
675
  // 找目标 channelId
@@ -605,58 +677,58 @@ export class MessageProcessor {
605
677
  if (isCrossChannel) {
606
678
  const targetAdapterName = targetInfo.adapter.channelName;
607
679
  const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
608
- const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName) ?? getOwner(this.config, targetAdapterName);
680
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName);
609
681
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
610
682
  if (!targetChannelId) {
611
- await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
683
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, subtype: 'channel_down' });
612
684
  continue;
613
685
  }
614
686
  }
615
687
  logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
616
688
  try {
617
- await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, taskReplyContext());
618
- 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 });
619
691
  if (isCrossChannel) {
620
- await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, taskReplyContext());
692
+ await adapter.send(envelope, { kind: 'system.notice', text: `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, subtype: 'health' });
621
693
  }
622
694
  }
623
695
  catch (error) {
624
696
  logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
625
- await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, taskReplyContext());
697
+ await adapter.send(envelope, { kind: 'system.error', text: `\u274c 文件发送失败: ${filePath}`, subtype: 'fatal' });
626
698
  }
627
699
  }
628
700
  } // end of !isProactive
629
- // 最终回复文本添加到 flusher(统一在流结束后处理,避免多 complete 事件重复发送)
701
+ // 最终回复文本添加到 renderer(统一在流结束后处理,避免多 complete 事件重复发送)
630
702
  // suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
631
703
  // 非 suppressed 且无流式文本:同上
632
704
  // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
633
- // 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
705
+ // 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
634
706
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
635
707
  // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
636
708
  // 特征:无流式 text + complete.result 匹配已知模式
637
709
  // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
638
- // Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
710
+ // Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
639
711
  const isSdkFallbackMessage = !!finalReplyText
640
712
  && !streamResult.hasReceivedText
641
713
  && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
642
714
  if (finalReplyText) {
643
715
  if (isProactive && isSdkFallbackMessage) {
644
- // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
716
+ // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent renderer
645
717
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
646
718
  if (!isCurrentlyBackground) {
647
- await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
719
+ await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
648
720
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
649
721
  }
650
722
  }
651
723
  else if (shouldSuppress()) {
652
- flusher.addText(finalReplyText);
724
+ renderer.addText(finalReplyText);
653
725
  }
654
- else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
655
- flusher.addText(finalReplyText);
726
+ else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
727
+ renderer.addText(finalReplyText);
656
728
  }
657
729
  }
658
730
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
659
- await flusher.flush(true);
731
+ await renderer.flush(true);
660
732
  // 清理 activeStreams(正常完成)
661
733
  agent.cleanupStream(streamKey);
662
734
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
@@ -676,9 +748,14 @@ export class MessageProcessor {
676
748
  const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
677
749
  const rawSubtype = streamResult.subtype || 'agent_error';
678
750
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
679
- 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
+ }
680
757
  this.eventBus.publish({
681
- type: 'message:error',
758
+ type: 'task:error',
682
759
  sessionId: session.id,
683
760
  error: errorSummary,
684
761
  errorType,
@@ -705,10 +782,30 @@ export class MessageProcessor {
705
782
  }
706
783
  else {
707
784
  // 真正的成功
708
- 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
+ }
709
806
  await this.sessionManager.recordSuccess(session.id);
710
807
  this.eventBus.publish({
711
- type: 'message:completed',
808
+ type: 'task:completed',
712
809
  sessionId: session.id,
713
810
  channel: message.channel,
714
811
  channelId: message.channelId,
@@ -726,12 +823,28 @@ export class MessageProcessor {
726
823
  status: 'completed',
727
824
  duration: Date.now() - startTime
728
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
+ }
729
842
  }
730
843
  const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
731
- if (isFinallyBackground) {
844
+ if (isFinallyBackground && session.sessionMode !== 'autonomous') {
732
845
  const projectName = path.basename(session.projectPath);
733
846
  const count = this.messageCache.getCount(session.id);
734
- 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' });
735
848
  }
736
849
  // 记录发送响应
737
850
  logger.message({
@@ -760,10 +873,12 @@ export class MessageProcessor {
760
873
  : 'error';
761
874
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
762
875
  if (!isUserInterrupt) {
763
- try {
764
- adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, taskReplyContext());
765
- }
766
- 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(() => { });
767
882
  }
768
883
  // 用户主动中断时降级日志;其余仍按 error 记录
769
884
  if (isUserInterrupt) {
@@ -775,7 +890,7 @@ export class MessageProcessor {
775
890
  const errorMsg = error instanceof Error ? error.message : String(error);
776
891
  const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
777
892
  this.eventBus.publish({
778
- type: 'message:error',
893
+ type: 'task:error',
779
894
  sessionId: session.id,
780
895
  error: errorMsg,
781
896
  errorType,
@@ -794,7 +909,7 @@ export class MessageProcessor {
794
909
  }
795
910
  // 发送用户友好的错误消息(SDK_TIMEOUT 已在 kill 级别发过提示,跳过)
796
911
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
797
- // processEventStream 已通过 flusher 发过错误时也跳过
912
+ // processEventStream 已通过 renderer 发过错误时也跳过
798
913
  if (error instanceof Error && error.message === 'SDK_TIMEOUT') {
799
914
  logger.info(`[MessageProcessor] SDK_TIMEOUT error, skip sending duplicate message`);
800
915
  }
@@ -802,14 +917,14 @@ export class MessageProcessor {
802
917
  logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
803
918
  }
804
919
  else if (error?._errorAlreadySent) {
805
- 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`);
806
921
  }
807
922
  else {
808
923
  const userMessage = getErrorMessage(error, undefined);
809
924
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
810
925
  let sendOpts;
811
926
  try {
812
- 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);
813
928
  sendOpts = this.getReplyContext(message);
814
929
  }
815
930
  catch { }
@@ -818,15 +933,8 @@ export class MessageProcessor {
818
933
  ...(sendOpts ?? {}),
819
934
  metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
820
935
  };
821
- await adapter.sendText(message.channelId, userMessage, sendOpts);
936
+ await adapter.send({ ...envelope, replyContext: sendOpts }, { kind: 'result.text', text: userMessage, isFinal: true });
822
937
  // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
823
- if (thoughtEmitter) {
824
- const thoughtErrorType = errType === ErrorType.CONTEXT_TOO_LONG ? 'context_too_long' :
825
- errType === ErrorType.AUTH_ERROR ? 'auth' :
826
- (errType === ErrorType.SDK_TIMEOUT || errType === ErrorType.STREAM_ERROR) ? 'network' :
827
- 'unknown';
828
- thoughtEmitter.emit({ type: 'error', error: userMessage, errorType: thoughtErrorType }).catch(() => { });
829
- }
830
938
  }
831
939
  }
832
940
  }
@@ -838,7 +946,22 @@ export class MessageProcessor {
838
946
  const metadata = (message.threadId && message.replyContext)
839
947
  ? { replyContext: message.replyContext }
840
948
  : undefined;
841
- 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);
842
965
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
843
966
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
844
967
  ? session.projectPath
@@ -851,9 +974,9 @@ export class MessageProcessor {
851
974
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
852
975
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
853
976
  */
854
- async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
977
+ async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
855
978
  // Per-session agent name for stats bucketing
856
- 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>';
857
980
  let hasReceivedText = false;
858
981
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
859
982
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -892,10 +1015,8 @@ export class MessageProcessor {
892
1015
  else {
893
1016
  logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
894
1017
  }
895
- // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
896
- if (thoughtEmitter) {
897
- thoughtEmitter.emit(event).catch(() => { });
898
- }
1018
+ // IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
1019
+ renderer.emit(event);
899
1020
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
900
1021
  if (event.type === 'session_id') {
901
1022
  logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
@@ -904,14 +1025,14 @@ export class MessageProcessor {
904
1025
  // session 状态变更(idle/running/requires_action)
905
1026
  if (event.type === 'state_changed') {
906
1027
  logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
907
- 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 });
908
1029
  continue;
909
1030
  }
910
1031
  // agent 状态通知(仅事件,不直出给用户)
911
1032
  if (event.type === 'status') {
912
1033
  logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
913
1034
  this.eventBus.publish({
914
- type: 'agent:status',
1035
+ type: 'runner:status',
915
1036
  sessionId: session.id,
916
1037
  subtype: event.subtype,
917
1038
  message: event.message,
@@ -928,14 +1049,14 @@ export class MessageProcessor {
928
1049
  lastReplyText += event.text;
929
1050
  this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
930
1051
  if (!shouldSuppress()) {
931
- flusher.addText(event.text);
1052
+ renderer.addText(event.text);
932
1053
  }
933
1054
  }
934
1055
  // compact 完成
935
1056
  if (event.type === 'compact') {
936
- 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 });
937
1058
  if (!shouldSuppress()) {
938
- flusher.addActivity(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`);
1059
+ renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
939
1060
  }
940
1061
  }
941
1062
  // 子任务进度
@@ -944,10 +1065,10 @@ export class MessageProcessor {
944
1065
  const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
945
1066
  const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
946
1067
  if (event.summary && !shouldSuppress()) {
947
- 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 });
948
1069
  }
949
1070
  else if (stats && !shouldSuppress()) {
950
- 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 });
951
1072
  }
952
1073
  }
953
1074
  // 工具调用
@@ -963,7 +1084,7 @@ export class MessageProcessor {
963
1084
  });
964
1085
  if (!shouldSuppress()) {
965
1086
  const desc = summarizeToolInput(event.name, event.input || {});
966
- flusher.addActivity(`\ud83d\udd27 ${event.name}${desc ? ': ' + desc : ''}`);
1087
+ renderer.addToolCall(event.name, event.input, event.callId, desc);
967
1088
  }
968
1089
  }
969
1090
  // 工具结果
@@ -982,7 +1103,10 @@ export class MessageProcessor {
982
1103
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
983
1104
  // 移除 XML 风格的错误标签
984
1105
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
985
- 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);
986
1110
  }
987
1111
  }
988
1112
  // 运行时错误(Codex: turn.failed / item error)
@@ -990,7 +1114,7 @@ export class MessageProcessor {
990
1114
  logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
991
1115
  if (!hasErrorResult && !shouldSuppress()) {
992
1116
  hasErrorResult = true;
993
- flusher.addActivity(`\u274c ${event.error}`);
1117
+ renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
994
1118
  }
995
1119
  }
996
1120
  // 完成事件
@@ -1015,12 +1139,12 @@ export class MessageProcessor {
1015
1139
  const userFriendlyMessage = event.terminalReason
1016
1140
  ? getErrorMessage(null, event.terminalReason)
1017
1141
  : `\u274c ${errorSummary}`;
1018
- flusher.addActivity(userFriendlyMessage);
1142
+ renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
1019
1143
  }
1020
1144
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
1021
1145
  // 最终文本留给流结束后的统一 flush(true)
1022
- if (flusher.hasContent()) {
1023
- await flusher.flushActivitiesOnly();
1146
+ if (renderer.hasContent()) {
1147
+ await renderer.flushActivitiesOnly();
1024
1148
  }
1025
1149
  }
1026
1150
  continue;
@@ -1054,7 +1178,7 @@ export class MessageProcessor {
1054
1178
  });
1055
1179
  // 后台任务完成也纳入统计
1056
1180
  this.eventBus.publish({
1057
- type: 'message:completed',
1181
+ type: 'task:completed',
1058
1182
  sessionId: session.id,
1059
1183
  channel: session.channel,
1060
1184
  channelId: session.channelId,
@@ -1076,7 +1200,7 @@ export class MessageProcessor {
1076
1200
  });
1077
1201
  // 后台任务失败也纳入统计
1078
1202
  this.eventBus.publish({
1079
- type: 'message:error',
1203
+ type: 'task:error',
1080
1204
  sessionId: session.id,
1081
1205
  error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
1082
1206
  errorType: bgErrorType,
@@ -1107,13 +1231,13 @@ export class MessageProcessor {
1107
1231
  logger.error('[MessageProcessor] Stream processing error:', error);
1108
1232
  }
1109
1233
  if (error instanceof Error && error.message.includes('process exited')) {
1110
- 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);
1111
1235
  }
1112
1236
  // Flush any pending error activities before re-throwing,
1113
1237
  // and mark the error so outer catch won't send a duplicate message
1114
- if (hasErrorResult || flusher.hasContent()) {
1238
+ if (hasErrorResult || renderer.hasContent()) {
1115
1239
  try {
1116
- await flusher.flush(true);
1240
+ await renderer.flush(true);
1117
1241
  }
1118
1242
  catch { }
1119
1243
  if (error instanceof Error) {
@@ -1247,8 +1371,65 @@ export class MessageProcessor {
1247
1371
  if (/^[.\s\u2026]+$/.test(filePath))
1248
1372
  return true;
1249
1373
  // 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
1250
- if (/[\\[\]{}*+?|^$]/.test(filePath))
1374
+ if (/[\[\]{}*+?|^$]/.test(filePath))
1251
1375
  return true;
1252
1376
  return false;
1253
1377
  }
1254
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
+ }