evolclaw 2.6.4 → 2.7.1

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);
@@ -8,6 +8,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
+ import { DEFAULT_PERMISSION_MODE } from '../../types.js';
11
12
  import { getOwner } from '../../config.js';
12
13
  import { getPackageRoot, resolveRoot } from '../../paths.js';
13
14
  import { renderPromptSection } from '../../prompts/templates.js';
@@ -130,7 +131,8 @@ export class MessageProcessor {
130
131
  '/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
131
132
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
132
133
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
133
- '/p ', '/s ', '/name ',
134
+ '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
135
+ '/aid', '/agentmd',
134
136
  ];
135
137
  /** 判断消息内容是否为已知命令 */
136
138
  isKnownCommand(content) {
@@ -193,6 +195,7 @@ export class MessageProcessor {
193
195
  logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
194
196
  });
195
197
  }
198
+ logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
196
199
  agent.interrupt(streamKey).catch(e => {
197
200
  logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
198
201
  });
@@ -383,12 +386,11 @@ export class MessageProcessor {
383
386
  ? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
384
387
  : undefined,
385
388
  });
386
- // 设置 per-session 权限模式(动态默认值:owner bypass,admin → auto,guest → noask)
387
- const role = session.identity?.role;
388
- const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'noask';
389
- agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
389
+ // 设置 per-session 权限模式(默认 bypass,所有角色统一)
390
+ agent.setMode(session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE);
390
391
  // 标记会话为处理中(实时持久化,重启后可恢复)
391
392
  this.sessionManager.markProcessing(session.id);
393
+ logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
392
394
  // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
393
395
  const prevInterruptReason = this.interruptedSessions.get(session.id);
394
396
  this.interruptedSessions.delete(session.id);
@@ -468,6 +470,7 @@ export class MessageProcessor {
468
470
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
469
471
  let streamRegistered = false;
470
472
  try {
473
+ logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
471
474
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
472
475
  agent.registerStream(streamKey, stream);
473
476
  streamRegistered = true;
@@ -594,8 +597,23 @@ export class MessageProcessor {
594
597
  // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
595
598
  // 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
596
599
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
600
+ // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
601
+ // 特征:无流式 text + complete.result 匹配已知模式
602
+ // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
603
+ // Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
604
+ const isSdkFallbackMessage = !!finalReplyText
605
+ && !streamResult.hasReceivedText
606
+ && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
597
607
  if (finalReplyText) {
598
- if (shouldSuppress()) {
608
+ if (isProactive && isSdkFallbackMessage) {
609
+ // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
610
+ const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
611
+ if (!isCurrentlyBackground) {
612
+ await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
613
+ logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
614
+ }
615
+ }
616
+ else if (shouldSuppress()) {
599
617
  flusher.addText(finalReplyText);
600
618
  }
601
619
  else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
@@ -606,8 +624,10 @@ export class MessageProcessor {
606
624
  await flusher.flush(true);
607
625
  // 清理 activeStreams(正常完成)
608
626
  agent.cleanupStream(streamKey);
627
+ logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
609
628
  // 清除处理中状态
610
629
  this.sessionManager.clearProcessing(session.id);
630
+ logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
611
631
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
612
632
  const interruptReason = this.interruptedSessions.get(session.id);
613
633
  if (streamResult.isError) {
@@ -681,8 +701,10 @@ export class MessageProcessor {
681
701
  catch (error) {
682
702
  // 清理流和处理中状态(异常时也要清除)
683
703
  agent.cleanupStream(streamKey);
704
+ logger.info(`[MessageProcessor] agent.cleanupStream ok (on error): session=${session.id} task=${taskId}`);
684
705
  try {
685
706
  this.sessionManager.clearProcessing(session.id);
707
+ logger.info(`[MessageProcessor] session ${session.id} processing cleared (on error) task=${taskId}`);
686
708
  }
687
709
  catch { }
688
710
  // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
@@ -796,7 +818,8 @@ export class MessageProcessor {
796
818
  // 每收到事件重置空闲超时
797
819
  const toolName = event.type === 'tool_use' ? event.name : undefined;
798
820
  resetTimer(event.type, toolName);
799
- // 记录所有事件类型(text / tool_use 附带摘要,便于排查)
821
+ // 记录事件类型:高价值事件(text/tool_use/tool_result/complete/error/compact/task_progress)INFO,
822
+ // 框架事件(session_id/state_changed/status)DEBUG
800
823
  let eventDetail = '';
801
824
  if (event.type === 'text' && event.text) {
802
825
  const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
@@ -813,25 +836,34 @@ export class MessageProcessor {
813
836
  || '';
814
837
  eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
815
838
  }
816
- logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
839
+ else if (event.type === 'tool_result') {
840
+ eventDetail = ` tool=${event.name} ok=${!event.isError}`;
841
+ }
842
+ const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
843
+ if (frameworkEvents.has(event.type)) {
844
+ logger.debug(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
845
+ }
846
+ else {
847
+ logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
848
+ }
817
849
  // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
818
850
  if (thoughtEmitter) {
819
851
  thoughtEmitter.emit(event).catch(() => { });
820
852
  }
821
853
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
822
854
  if (event.type === 'session_id') {
823
- logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
855
+ logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
824
856
  continue;
825
857
  }
826
858
  // session 状态变更(idle/running/requires_action)
827
859
  if (event.type === 'state_changed') {
828
- logger.info(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
860
+ logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
829
861
  this.eventBus.publish({ type: 'agent:state-changed', sessionId: session.id, state: event.state });
830
862
  continue;
831
863
  }
832
864
  // agent 状态通知(仅事件,不直出给用户)
833
865
  if (event.type === 'status') {
834
- logger.info(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
866
+ logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
835
867
  this.eventBus.publish({
836
868
  type: 'agent:status',
837
869
  sessionId: session.id,
@@ -890,7 +922,6 @@ export class MessageProcessor {
890
922
  }
891
923
  // 工具结果
892
924
  if (event.type === 'tool_result') {
893
- logger.debug(`[MessageProcessor] tool_result: name=${event.name}, is_error=${event.isError}`);
894
925
  this.eventBus.publish({
895
926
  type: 'tool:result',
896
927
  sessionId: session.id,
@@ -1,4 +1,5 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
+ import { DEFAULT_PERMISSION_MODE } from '../../types.js';
2
3
  import { ensureDir } from '../../config.js';
3
4
  import { resolvePaths } from '../../paths.js';
4
5
  import { logger } from '../../utils/logger.js';
@@ -315,15 +316,15 @@ export class SessionManager {
315
316
  logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
316
317
  }
317
318
  }
318
- // Migration: readonly 模式已暂时禁用,历史会话统一转为 noask
319
+ // Migration: readonly 模式已禁用,历史会话统一转为 auto
319
320
  if (hasMetadata && tableInfo.length > 0) {
320
321
  const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
321
322
  let migratedPerm = 0;
322
323
  for (const row of rows) {
323
324
  try {
324
325
  const meta = JSON.parse(row.metadata);
325
- if (meta.permissionMode === 'readonly') {
326
- meta.permissionMode = 'noask';
326
+ if (meta.permissionMode === 'readonly' || meta.permissionMode === 'noask') {
327
+ meta.permissionMode = 'auto';
327
328
  this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
328
329
  .run(JSON.stringify(meta), row.id);
329
330
  migratedPerm++;
@@ -332,7 +333,7 @@ export class SessionManager {
332
333
  catch { /* skip malformed JSON */ }
333
334
  }
334
335
  if (migratedPerm > 0) {
335
- logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode readonly noask`);
336
+ logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode → auto`);
336
337
  }
337
338
  }
338
339
  // 创建新表(首次初始化)
@@ -471,7 +472,7 @@ export class SessionManager {
471
472
  session.identity = this.resolveIdentity(channel, userId);
472
473
  // 新话题会话补写默认权限模式
473
474
  if (session.metadata && !session.metadata.permissionMode) {
474
- session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
475
+ session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
475
476
  this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
476
477
  .run(JSON.stringify(session.metadata), Date.now(), session.id);
477
478
  }
@@ -580,9 +581,9 @@ export class SessionManager {
580
581
  updatedAt: Date.now()
581
582
  };
582
583
  session.identity = this.resolveIdentity(channel, userId);
583
- // 写入默认权限模式(基于角色,只在首次创建时设置)
584
+ // 写入默认权限模式(统一 bypass,只在首次创建时设置)
584
585
  if (!sessionMetadata.permissionMode) {
585
- sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
586
+ sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
586
587
  }
587
588
  this.insertSession(session);
588
589
  this.eventBus.publish({
@@ -681,6 +682,7 @@ export class SessionManager {
681
682
  }
682
683
  async switchProject(channel, channelId, newProjectPath, currentAgentId) {
683
684
  const agentId = currentAgentId || 'claude';
685
+ logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
684
686
  // 1. 继承当前 chatType(在 deactivate 之前读取)
685
687
  const inheritedChatType = this.getActiveChatType(channel, channelId);
686
688
  // 2. 取消当前活跃会话