evolclaw 2.6.4 → 2.7.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.
package/dist/config.js CHANGED
@@ -51,7 +51,7 @@ function loadCodexSettings() {
51
51
  export function resolveAnthropicConfig(config) {
52
52
  const settings = loadClaudeSettings();
53
53
  // 过滤占位符,视为未配置
54
- const configApiKey = config.agents?.anthropic?.apiKey;
54
+ const configApiKey = config.agents?.claude?.apiKey;
55
55
  const isPlaceholderKey = !configApiKey ||
56
56
  configApiKey.includes('your-') ||
57
57
  configApiKey.includes('placeholder');
@@ -59,21 +59,21 @@ export function resolveAnthropicConfig(config) {
59
59
  || process.env.ANTHROPIC_AUTH_TOKEN
60
60
  || settings.env?.ANTHROPIC_AUTH_TOKEN;
61
61
  if (!apiKey) {
62
- throw new Error('No API key found. Set one of: agents.anthropic.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
62
+ throw new Error('No API key found. Set one of: agents.claude.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
63
63
  }
64
64
  // baseUrl 也过滤占位符
65
- const configBaseUrl = config.agents?.anthropic?.baseUrl;
65
+ const configBaseUrl = config.agents?.claude?.baseUrl;
66
66
  const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
67
67
  const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
68
68
  || process.env.ANTHROPIC_BASE_URL
69
69
  || settings.env?.ANTHROPIC_BASE_URL;
70
- const model = config.agents?.anthropic?.model
70
+ const model = config.agents?.claude?.model
71
71
  || settings.model
72
72
  || 'sonnet';
73
- const effort = config.agents?.anthropic?.effort
73
+ const effort = config.agents?.claude?.effort
74
74
  || settings.effortLevel
75
75
  || undefined;
76
- const configExecPath = config.agents?.anthropic?.pathToClaudeCodeExecutable;
76
+ const configExecPath = config.agents?.claude?.pathToClaudeCodeExecutable;
77
77
  const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
78
78
  const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
79
79
  return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
@@ -81,7 +81,7 @@ export function resolveAnthropicConfig(config) {
81
81
  export function resolveOpenaiConfig(config) {
82
82
  const codexSettings = loadCodexSettings();
83
83
  // 过滤占位符,视为未配置
84
- const configApiKey = config.agents?.openai?.apiKey;
84
+ const configApiKey = config.agents?.codex?.apiKey;
85
85
  const isPlaceholderKey = !configApiKey ||
86
86
  configApiKey.includes('your-') ||
87
87
  configApiKey.includes('placeholder');
@@ -89,23 +89,23 @@ export function resolveOpenaiConfig(config) {
89
89
  || process.env.OPENAI_API_KEY
90
90
  || codexSettings.apiKey;
91
91
  if (!apiKey) {
92
- throw new Error('No OpenAI API key found. Set one of: agents.openai.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
92
+ throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
93
93
  }
94
94
  // baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
95
- const configBaseUrl = config.agents?.openai?.baseUrl;
95
+ const configBaseUrl = config.agents?.codex?.baseUrl;
96
96
  const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
97
97
  const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
98
98
  || process.env.OPENAI_BASE_URL
99
99
  || codexSettings.baseUrl
100
100
  || undefined;
101
- const model = config.agents?.openai?.model
101
+ const model = config.agents?.codex?.model
102
102
  || codexSettings.model
103
103
  || 'gpt-5.2-codex';
104
- const effort = config.agents?.openai?.effort || config.agents?.openai?.reasoning || undefined;
104
+ const effort = config.agents?.codex?.effort || config.agents?.codex?.reasoning || undefined;
105
105
  return { apiKey, baseUrl, model, effort };
106
106
  }
107
107
  export function resolveGoogleConfig(config) {
108
- const googleCfg = config.agents?.google;
108
+ const googleCfg = config.agents?.gemini;
109
109
  // CLI path: config → which gemini
110
110
  let cliPath = googleCfg?.cliPath || '';
111
111
  if (!cliPath) {
@@ -170,9 +170,41 @@ export function loadConfig(configPath = resolvePaths().config) {
170
170
  }
171
171
  const content = fs.readFileSync(configPath, 'utf-8');
172
172
  const config = JSON.parse(content);
173
+ if (migrateAgentsKeys(config)) {
174
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
175
+ logger.warn(`Config migrated: agents.{anthropic,openai,google} → {claude,codex,gemini} in ${configPath}`);
176
+ }
173
177
  validateConfig(config);
174
178
  return config;
175
179
  }
180
+ /**
181
+ * Rename legacy agent config keys to runner names.
182
+ * Returns true if any rename happened.
183
+ */
184
+ function migrateAgentsKeys(config) {
185
+ const agents = config?.agents;
186
+ if (!agents || typeof agents !== 'object')
187
+ return false;
188
+ const renames = [
189
+ ['anthropic', 'claude'],
190
+ ['openai', 'codex'],
191
+ ['google', 'gemini'],
192
+ ];
193
+ let changed = false;
194
+ for (const [oldKey, newKey] of renames) {
195
+ if (agents[oldKey] === undefined)
196
+ continue;
197
+ if (agents[newKey] === undefined) {
198
+ agents[newKey] = agents[oldKey];
199
+ }
200
+ else {
201
+ logger.warn(`Config has both agents.${oldKey} and agents.${newKey}; keeping new key, dropping legacy one`);
202
+ }
203
+ delete agents[oldKey];
204
+ changed = true;
205
+ }
206
+ return changed;
207
+ }
176
208
  export function saveConfig(config, configPath = resolvePaths().config) {
177
209
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
178
210
  }
@@ -309,26 +341,16 @@ export function setChannelShowActivities(config, instanceName, mode) {
309
341
  }
310
342
  }
311
343
  /**
312
- * 读取通道实例的 sessionMode 锁定配置
313
- * 返回 undefined 表示未配置(由 session-manager chatType 默认决定)
344
+ * 读取全局 chatmode 配置的默认 sessionMode
345
+ * chatType 返回对应模式,未配置时返回 undefined(由 session-manager 回退到 'interactive')
314
346
  */
315
- export function getChannelSessionMode(config, instanceName) {
316
- for (const type of channelTypes) {
317
- const raw = config.channels?.[type];
318
- if (raw === undefined)
319
- continue;
320
- if (Array.isArray(raw)) {
321
- const inst = raw.find((item) => item.name === instanceName);
322
- if (inst)
323
- return inst.sessionMode;
324
- }
325
- else {
326
- const effectiveName = raw.name ?? type;
327
- if (effectiveName === instanceName)
328
- return raw.sessionMode;
329
- }
330
- }
331
- return undefined;
347
+ export function getDefaultSessionMode(config, chatType) {
348
+ const cm = config.chatmode;
349
+ if (!cm)
350
+ return undefined;
351
+ if (chatType === 'group')
352
+ return cm.group;
353
+ return cm.private;
332
354
  }
333
355
  export function isOwner(config, channelOrType, userId) {
334
356
  // 按实例名精确匹配
@@ -441,29 +463,33 @@ export function ensureDir(dirPath) {
441
463
  fs.mkdirSync(dirPath, { recursive: true });
442
464
  }
443
465
  }
444
- // agents.defaultAgent → config key 映射
445
- const agentKeyMap = { claude: 'anthropic', codex: 'openai', gemini: 'google' };
446
466
  /**
447
467
  * 配置结构完整性校验(不校验凭据有效性)。
448
468
  * 要求 agents/channels/projects 三段同时具备必要的锚点字段。
449
469
  */
450
470
  export function validateConfigIntegrity(config) {
451
471
  const reasons = [];
452
- // agents
472
+ // agents — 单 agent 时自动推断,无需显式 defaultAgent
453
473
  const defaultAgent = config.agents?.defaultAgent;
454
474
  if (!defaultAgent) {
455
- reasons.push('Missing agents.defaultAgent');
475
+ const agentKeys = Object.keys(config.agents || {}).filter(k => k !== 'defaultAgent');
476
+ const configuredAgents = agentKeys.filter(k => config.agents?.[k]);
477
+ if (configuredAgents.length === 0 && agentKeys.length !== 1) {
478
+ reasons.push('Missing agents.defaultAgent (multiple or no agents configured)');
479
+ }
456
480
  }
457
481
  else {
458
- const key = agentKeyMap[defaultAgent] || defaultAgent;
459
- if (!config.agents?.[key]) {
460
- reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${key} does not exist`);
482
+ if (!config.agents?.[defaultAgent]) {
483
+ reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${defaultAgent} does not exist`);
461
484
  }
462
485
  }
463
- // channels
486
+ // channels — 单通道时自动推断,无需显式 defaultChannel
464
487
  const defaultChannel = config.channels?.defaultChannel;
465
488
  if (!defaultChannel) {
466
- reasons.push('Missing channels.defaultChannel');
489
+ const channelKeys = channelTypes.filter(t => config.channels?.[t]);
490
+ if (channelKeys.length === 0) {
491
+ reasons.push('Missing channels.defaultChannel (no channels configured)');
492
+ }
467
493
  }
468
494
  else {
469
495
  if (!config.channels?.[defaultChannel]) {
@@ -1,5 +1,5 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
- import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities, getChannelSessionMode } from '../config.js';
2
+ import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import crypto from 'crypto';
5
5
  import path from 'path';
@@ -112,7 +112,7 @@ const aliases = {
112
112
  '/rw': '/rewind'
113
113
  };
114
114
  // 命令快速路径前缀(所有命令都不进入消息队列)
115
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode'];
115
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd'];
116
116
  export class CommandHandler {
117
117
  sessionManager;
118
118
  config;
@@ -150,9 +150,15 @@ export class CommandHandler {
150
150
  this.defaultAgentId = agentRunnerOrMap.name;
151
151
  }
152
152
  }
153
- /** 项目列表快捷访问 */
153
+ /** 项目列表快捷访问(list 缺失时用 defaultPath 作为唯一项目) */
154
154
  get projects() {
155
- return this.config.projects?.list || {};
155
+ const list = this.config.projects?.list;
156
+ if (list && Object.keys(list).length > 0)
157
+ return list;
158
+ const dp = this.config.projects?.defaultPath;
159
+ if (dp)
160
+ return { [path.basename(dp)]: dp };
161
+ return {};
156
162
  }
157
163
  /** 根据项目路径查找配置中的项目名称 */
158
164
  getConfiguredProjectName(projectPath) {
@@ -532,6 +538,7 @@ export class CommandHandler {
532
538
  if (normalizedContent !== content) {
533
539
  logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
534
540
  }
541
+ logger.info(`[CommandHandler] handle: channel=${channel} channelId=${channelId} cmd="${normalizedContent.split(' ')[0]}" user=${userId ?? 'n/a'} role=${identity?.role ?? 'n/a'}`);
535
542
  // 话题内禁用部分命令
536
543
  if (threadId) {
537
544
  const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
@@ -701,7 +708,7 @@ export class CommandHandler {
701
708
  if (!hasPermissionController(permAgent)) {
702
709
  return '❌ 权限控制不可用';
703
710
  }
704
- const defaultPermMode = identity.role === 'owner' ? 'bypass' : identity.role === 'admin' ? 'auto' : 'noask';
711
+ const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'auto';
705
712
  const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
706
713
  const modes = permAgent.listModes();
707
714
  // 尝试发送交互卡片
@@ -737,6 +744,10 @@ export class CommandHandler {
737
744
  const adapter = this.adapters.get(channel);
738
745
  adapter?.sendText(channelId, result, replyCtx);
739
746
  }
747
+ else {
748
+ // 切换成功后重新发新卡片(会自动 invalidate 旧卡片)
749
+ await this.handle('/perm', channel, channelId, undefined, userId, threadId);
750
+ }
740
751
  }
741
752
  },
742
753
  });
@@ -1008,14 +1019,14 @@ export class CommandHandler {
1008
1019
  // evolclaw.json 配了 → 写 evolclaw.json
1009
1020
  // evolclaw.json 没配 → 写 agent 全局配置
1010
1021
  if (isCodexAgent) {
1011
- const configuredInEvolclaw = !!(this.config.agents?.openai?.model || this.config.agents?.openai?.reasoning);
1022
+ const configuredInEvolclaw = !!(this.config.agents?.codex?.model || this.config.agents?.codex?.reasoning);
1012
1023
  if (configuredInEvolclaw) {
1013
- if (!this.config.agents.openai)
1014
- this.config.agents.openai = {};
1024
+ if (!this.config.agents.codex)
1025
+ this.config.agents.codex = {};
1015
1026
  if (newModel)
1016
- this.config.agents.openai.model = newModel;
1027
+ this.config.agents.codex.model = newModel;
1017
1028
  if (newEffort)
1018
- this.config.agents.openai.reasoning = newEffort;
1029
+ this.config.agents.codex.reasoning = newEffort;
1019
1030
  try {
1020
1031
  saveConfig(this.config);
1021
1032
  }
@@ -1025,12 +1036,12 @@ export class CommandHandler {
1025
1036
  }
1026
1037
  else {
1027
1038
  // Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
1028
- if (!this.config.agents.openai)
1029
- this.config.agents.openai = {};
1039
+ if (!this.config.agents.codex)
1040
+ this.config.agents.codex = {};
1030
1041
  if (newModel)
1031
- this.config.agents.openai.model = newModel;
1042
+ this.config.agents.codex.model = newModel;
1032
1043
  if (newEffort)
1033
- this.config.agents.openai.reasoning = newEffort;
1044
+ this.config.agents.codex.reasoning = newEffort;
1034
1045
  try {
1035
1046
  saveConfig(this.config);
1036
1047
  }
@@ -1040,14 +1051,14 @@ export class CommandHandler {
1040
1051
  }
1041
1052
  }
1042
1053
  else {
1043
- const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
1054
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1044
1055
  if (configuredInEvolclaw) {
1045
- if (!this.config.agents.anthropic)
1046
- this.config.agents.anthropic = {};
1056
+ if (!this.config.agents.claude)
1057
+ this.config.agents.claude = {};
1047
1058
  if (newModel)
1048
- this.config.agents.anthropic.model = newModel;
1059
+ this.config.agents.claude.model = newModel;
1049
1060
  if (newEffort)
1050
- this.config.agents.anthropic.effort = newEffort;
1061
+ this.config.agents.claude.effort = newEffort;
1051
1062
  try {
1052
1063
  saveConfig(this.config);
1053
1064
  }
@@ -1146,8 +1157,8 @@ export class CommandHandler {
1146
1157
  effortAgent.setEffort?.(undefined);
1147
1158
  const isCodex = effortAgent.name === 'codex';
1148
1159
  if (isCodex) {
1149
- if (this.config.agents?.openai?.reasoning) {
1150
- delete this.config.agents.openai.reasoning;
1160
+ if (this.config.agents?.codex?.reasoning) {
1161
+ delete this.config.agents.codex.reasoning;
1151
1162
  try {
1152
1163
  saveConfig(this.config);
1153
1164
  }
@@ -1155,9 +1166,9 @@ export class CommandHandler {
1155
1166
  }
1156
1167
  }
1157
1168
  else {
1158
- const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
1169
+ const configuredInEvolclaw = !!this.config.agents?.claude?.effort;
1159
1170
  if (configuredInEvolclaw) {
1160
- delete this.config.agents.anthropic.effort;
1171
+ delete this.config.agents.claude.effort;
1161
1172
  try {
1162
1173
  saveConfig(this.config);
1163
1174
  }
@@ -1183,20 +1194,20 @@ export class CommandHandler {
1183
1194
  this.config.agents = {};
1184
1195
  const isCodex = effortAgent.name === 'codex';
1185
1196
  if (isCodex) {
1186
- if (!this.config.agents.openai)
1187
- this.config.agents.openai = {};
1188
- this.config.agents.openai.reasoning = newEffort;
1197
+ if (!this.config.agents.codex)
1198
+ this.config.agents.codex = {};
1199
+ this.config.agents.codex.reasoning = newEffort;
1189
1200
  try {
1190
1201
  saveConfig(this.config);
1191
1202
  }
1192
1203
  catch { }
1193
1204
  }
1194
1205
  else {
1195
- const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
1206
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1196
1207
  if (configuredInEvolclaw) {
1197
- if (!this.config.agents.anthropic)
1198
- this.config.agents.anthropic = {};
1199
- this.config.agents.anthropic.effort = newEffort;
1208
+ if (!this.config.agents.claude)
1209
+ this.config.agents.claude = {};
1210
+ this.config.agents.claude.effort = newEffort;
1200
1211
  try {
1201
1212
  saveConfig(this.config);
1202
1213
  }
@@ -1456,16 +1467,14 @@ export class CommandHandler {
1456
1467
  if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1457
1468
  if (!activeSession)
1458
1469
  return '❌ 当前无活跃会话';
1459
- const lockedMode = getChannelSessionMode(this.config, channel);
1460
1470
  const arg = normalizedContent.slice(9).trim();
1461
1471
  const currentMode = activeSession.sessionMode || 'interactive';
1462
1472
  if (!arg) {
1463
- const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
1464
1473
  const canSwitch = activeChatType !== 'group' || isAdmin;
1465
- if (canSwitch && !lockedMode) {
1466
- return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1474
+ if (canSwitch) {
1475
+ return `📋 当前会话模式: ${currentMode}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1467
1476
  }
1468
- return `📋 当前会话模式: ${currentMode}${lockHint}`;
1477
+ return `📋 当前会话模式: ${currentMode}`;
1469
1478
  }
1470
1479
  if (arg !== 'interactive' && arg !== 'proactive') {
1471
1480
  return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
@@ -1473,9 +1482,6 @@ export class CommandHandler {
1473
1482
  if (activeChatType === 'group' && !isAdmin) {
1474
1483
  return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
1475
1484
  }
1476
- if (lockedMode) {
1477
- return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
1478
- }
1479
1485
  if (arg === currentMode) {
1480
1486
  return `📋 当前会话模式已是 ${arg}`;
1481
1487
  }
@@ -1637,8 +1643,7 @@ export class CommandHandler {
1637
1643
  }
1638
1644
  const lines = [];
1639
1645
  const sessionMode = session.sessionMode || 'interactive';
1640
- const lockedMode = getChannelSessionMode(this.config, channel);
1641
- const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
1646
+ const chatModeLine = `会话模式: ${sessionMode}`;
1642
1647
  if (isAdmin) {
1643
1648
  lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
1644
1649
  if (health.consecutiveErrors > 0) {
@@ -2175,7 +2180,12 @@ export class CommandHandler {
2175
2180
  return '❌ 项目路径必须是绝对路径';
2176
2181
  }
2177
2182
  if (!fs.existsSync(projectPath)) {
2178
- return `❌ 路径不存在: ${projectPath}`;
2183
+ if (this.config.projects?.autoCreate) {
2184
+ fs.mkdirSync(projectPath, { recursive: true });
2185
+ }
2186
+ else {
2187
+ return `❌ 路径不存在: ${projectPath}`;
2188
+ }
2179
2189
  }
2180
2190
  // 生成项目名称(使用目录名)
2181
2191
  const projectName = path.basename(projectPath);
@@ -130,7 +130,8 @@ export class MessageProcessor {
130
130
  '/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
131
131
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
132
132
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
133
- '/p ', '/s ', '/name ',
133
+ '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
134
+ '/aid', '/agentmd',
134
135
  ];
135
136
  /** 判断消息内容是否为已知命令 */
136
137
  isKnownCommand(content) {
@@ -193,6 +194,7 @@ export class MessageProcessor {
193
194
  logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
194
195
  });
195
196
  }
197
+ logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
196
198
  agent.interrupt(streamKey).catch(e => {
197
199
  logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
198
200
  });
@@ -383,12 +385,13 @@ export class MessageProcessor {
383
385
  ? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
384
386
  : undefined,
385
387
  });
386
- // 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → noask
388
+ // 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → auto
387
389
  const role = session.identity?.role;
388
- const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'noask';
390
+ const defaultPermMode = role === 'owner' ? 'bypass' : 'auto';
389
391
  agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
390
392
  // 标记会话为处理中(实时持久化,重启后可恢复)
391
393
  this.sessionManager.markProcessing(session.id);
394
+ logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
392
395
  // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
393
396
  const prevInterruptReason = this.interruptedSessions.get(session.id);
394
397
  this.interruptedSessions.delete(session.id);
@@ -468,6 +471,7 @@ export class MessageProcessor {
468
471
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
469
472
  let streamRegistered = false;
470
473
  try {
474
+ logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
471
475
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
472
476
  agent.registerStream(streamKey, stream);
473
477
  streamRegistered = true;
@@ -594,8 +598,23 @@ export class MessageProcessor {
594
598
  // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
595
599
  // 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
596
600
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
601
+ // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
602
+ // 特征:无流式 text + complete.result 匹配已知模式
603
+ // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
604
+ // Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
605
+ const isSdkFallbackMessage = !!finalReplyText
606
+ && !streamResult.hasReceivedText
607
+ && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
597
608
  if (finalReplyText) {
598
- if (shouldSuppress()) {
609
+ if (isProactive && isSdkFallbackMessage) {
610
+ // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
611
+ const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
612
+ if (!isCurrentlyBackground) {
613
+ await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
614
+ logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
615
+ }
616
+ }
617
+ else if (shouldSuppress()) {
599
618
  flusher.addText(finalReplyText);
600
619
  }
601
620
  else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
@@ -606,8 +625,10 @@ export class MessageProcessor {
606
625
  await flusher.flush(true);
607
626
  // 清理 activeStreams(正常完成)
608
627
  agent.cleanupStream(streamKey);
628
+ logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
609
629
  // 清除处理中状态
610
630
  this.sessionManager.clearProcessing(session.id);
631
+ logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
611
632
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
612
633
  const interruptReason = this.interruptedSessions.get(session.id);
613
634
  if (streamResult.isError) {
@@ -681,8 +702,10 @@ export class MessageProcessor {
681
702
  catch (error) {
682
703
  // 清理流和处理中状态(异常时也要清除)
683
704
  agent.cleanupStream(streamKey);
705
+ logger.info(`[MessageProcessor] agent.cleanupStream ok (on error): session=${session.id} task=${taskId}`);
684
706
  try {
685
707
  this.sessionManager.clearProcessing(session.id);
708
+ logger.info(`[MessageProcessor] session ${session.id} processing cleared (on error) task=${taskId}`);
686
709
  }
687
710
  catch { }
688
711
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
@@ -796,7 +819,8 @@ export class MessageProcessor {
796
819
  // 每收到事件重置空闲超时
797
820
  const toolName = event.type === 'tool_use' ? event.name : undefined;
798
821
  resetTimer(event.type, toolName);
799
- // 记录所有事件类型(text / tool_use 附带摘要,便于排查)
822
+ // 记录事件类型:高价值事件(text/tool_use/tool_result/complete/error/compact/task_progress)INFO,
823
+ // 框架事件(session_id/state_changed/status)DEBUG
800
824
  let eventDetail = '';
801
825
  if (event.type === 'text' && event.text) {
802
826
  const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
@@ -813,25 +837,34 @@ export class MessageProcessor {
813
837
  || '';
814
838
  eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
815
839
  }
816
- logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
840
+ else if (event.type === 'tool_result') {
841
+ eventDetail = ` tool=${event.name} ok=${!event.isError}`;
842
+ }
843
+ const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
844
+ if (frameworkEvents.has(event.type)) {
845
+ logger.debug(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
846
+ }
847
+ else {
848
+ logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
849
+ }
817
850
  // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
818
851
  if (thoughtEmitter) {
819
852
  thoughtEmitter.emit(event).catch(() => { });
820
853
  }
821
854
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
822
855
  if (event.type === 'session_id') {
823
- logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
856
+ logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
824
857
  continue;
825
858
  }
826
859
  // session 状态变更(idle/running/requires_action)
827
860
  if (event.type === 'state_changed') {
828
- logger.info(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
861
+ logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
829
862
  this.eventBus.publish({ type: 'agent:state-changed', sessionId: session.id, state: event.state });
830
863
  continue;
831
864
  }
832
865
  // agent 状态通知(仅事件,不直出给用户)
833
866
  if (event.type === 'status') {
834
- logger.info(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
867
+ logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
835
868
  this.eventBus.publish({
836
869
  type: 'agent:status',
837
870
  sessionId: session.id,
@@ -890,7 +923,6 @@ export class MessageProcessor {
890
923
  }
891
924
  // 工具结果
892
925
  if (event.type === 'tool_result') {
893
- logger.debug(`[MessageProcessor] tool_result: name=${event.name}, is_error=${event.isError}`);
894
926
  this.eventBus.publish({
895
927
  type: 'tool:result',
896
928
  sessionId: session.id,
@@ -315,15 +315,15 @@ export class SessionManager {
315
315
  logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
316
316
  }
317
317
  }
318
- // Migration: readonly 模式已暂时禁用,历史会话统一转为 noask
318
+ // Migration: readonly 模式已禁用,历史会话统一转为 auto
319
319
  if (hasMetadata && tableInfo.length > 0) {
320
320
  const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
321
321
  let migratedPerm = 0;
322
322
  for (const row of rows) {
323
323
  try {
324
324
  const meta = JSON.parse(row.metadata);
325
- if (meta.permissionMode === 'readonly') {
326
- meta.permissionMode = 'noask';
325
+ if (meta.permissionMode === 'readonly' || meta.permissionMode === 'noask') {
326
+ meta.permissionMode = 'auto';
327
327
  this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
328
328
  .run(JSON.stringify(meta), row.id);
329
329
  migratedPerm++;
@@ -332,7 +332,7 @@ export class SessionManager {
332
332
  catch { /* skip malformed JSON */ }
333
333
  }
334
334
  if (migratedPerm > 0) {
335
- logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode readonly noask`);
335
+ logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode → auto`);
336
336
  }
337
337
  }
338
338
  // 创建新表(首次初始化)
@@ -471,7 +471,7 @@ export class SessionManager {
471
471
  session.identity = this.resolveIdentity(channel, userId);
472
472
  // 新话题会话补写默认权限模式
473
473
  if (session.metadata && !session.metadata.permissionMode) {
474
- session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
474
+ session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
475
475
  this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
476
476
  .run(JSON.stringify(session.metadata), Date.now(), session.id);
477
477
  }
@@ -582,7 +582,7 @@ export class SessionManager {
582
582
  session.identity = this.resolveIdentity(channel, userId);
583
583
  // 写入默认权限模式(基于角色,只在首次创建时设置)
584
584
  if (!sessionMetadata.permissionMode) {
585
- sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
585
+ sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
586
586
  }
587
587
  this.insertSession(session);
588
588
  this.eventBus.publish({
@@ -681,6 +681,7 @@ export class SessionManager {
681
681
  }
682
682
  async switchProject(channel, channelId, newProjectPath, currentAgentId) {
683
683
  const agentId = currentAgentId || 'claude';
684
+ logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
684
685
  // 1. 继承当前 chatType(在 deactivate 之前读取)
685
686
  const inheritedChatType = this.getActiveChatType(channel, channelId);
686
687
  // 2. 取消当前活跃会话