metame-cli 1.4.33 → 1.5.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 (44) hide show
  1. package/README.md +187 -48
  2. package/index.js +148 -9
  3. package/package.json +6 -3
  4. package/scripts/daemon-admin-commands.js +254 -9
  5. package/scripts/daemon-agent-commands.js +64 -6
  6. package/scripts/daemon-agent-tools.js +26 -5
  7. package/scripts/daemon-bridges.js +110 -20
  8. package/scripts/daemon-claude-engine.js +704 -268
  9. package/scripts/daemon-command-router.js +24 -8
  10. package/scripts/daemon-default.yaml +28 -4
  11. package/scripts/daemon-engine-runtime.js +275 -0
  12. package/scripts/daemon-exec-commands.js +10 -4
  13. package/scripts/daemon-notify.js +37 -1
  14. package/scripts/daemon-runtime-lifecycle.js +2 -1
  15. package/scripts/daemon-session-commands.js +52 -4
  16. package/scripts/daemon-session-store.js +2 -1
  17. package/scripts/daemon-task-scheduler.js +87 -28
  18. package/scripts/daemon-user-acl.js +26 -9
  19. package/scripts/daemon.js +81 -17
  20. package/scripts/distill.js +323 -18
  21. package/scripts/docs/agent-guide.md +12 -0
  22. package/scripts/docs/maintenance-manual.md +119 -0
  23. package/scripts/docs/pointer-map.md +88 -0
  24. package/scripts/feishu-adapter.js +6 -1
  25. package/scripts/hooks/stop-session-capture.js +243 -0
  26. package/scripts/memory-extract.js +100 -5
  27. package/scripts/memory-nightly-reflect.js +196 -11
  28. package/scripts/memory.js +134 -3
  29. package/scripts/mentor-engine.js +405 -0
  30. package/scripts/platform.js +2 -0
  31. package/scripts/providers.js +169 -21
  32. package/scripts/schema.js +12 -0
  33. package/scripts/session-analytics.js +245 -12
  34. package/scripts/skill-changelog.js +245 -0
  35. package/scripts/skill-evolution.js +288 -5
  36. package/scripts/usage-classifier.js +1 -1
  37. package/scripts/daemon-admin-commands.test.js +0 -333
  38. package/scripts/daemon-task-envelope.test.js +0 -59
  39. package/scripts/daemon-task-scheduler.test.js +0 -106
  40. package/scripts/reliability-core.test.js +0 -280
  41. package/scripts/skill-evolution.test.js +0 -113
  42. package/scripts/task-board.test.js +0 -83
  43. package/scripts/test_daemon.js +0 -1407
  44. package/scripts/utils.test.js +0 -192
@@ -26,6 +26,7 @@ function createCommandRouter(deps) {
26
26
  pendingAgentFlows,
27
27
  pendingActivations,
28
28
  agentFlowTtlMs,
29
+ getDefaultEngine,
29
30
  } = deps;
30
31
 
31
32
  function resolveFlowTtlMs() {
@@ -178,6 +179,13 @@ function createCommandRouter(deps) {
178
179
  return '';
179
180
  }
180
181
 
182
+ function inferAgentEngineFromText(input) {
183
+ const text = String(input || '').trim().toLowerCase();
184
+ if (!text) return null;
185
+ if (/\bcodex\b/.test(text) || /柯德|科德/.test(text)) return 'codex';
186
+ return null;
187
+ }
188
+
181
189
  function isLikelyDirectAgentAction(input) {
182
190
  const text = String(input || '').trim();
183
191
  return /^(?:请|帮我|麻烦|给我|给这个群|给当前群|在这个群|把这个群|把当前群|将这个群|这个群|当前群|本群|群里|我想|我要|我需要|创建|新建|新增|搞一个|加一个|create|bind|绑定|列出|查看|显示|有哪些|解绑|取消绑定|断开绑定|修改|调整)/i.test(text);
@@ -435,14 +443,19 @@ function createCommandRouter(deps) {
435
443
  }
436
444
  const agentName = deriveAgentName(input, workspaceDir);
437
445
  const roleDelta = deriveCreateRoleDelta(input);
446
+ const inferredEngine = inferAgentEngineFromText(input);
438
447
  // Always skip binding creating chat — new group activates via /activate
439
- const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, { skipChatBinding: true });
448
+ const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, {
449
+ skipChatBinding: true,
450
+ engine: inferredEngine,
451
+ });
440
452
  if (!res.ok) {
441
453
  await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
442
454
  return true;
443
455
  }
444
456
  const data = res.data || {};
445
457
  const projName = projectNameFromResult(data, agentName);
458
+ const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
446
459
  if (data.projectKey && pendingActivations) {
447
460
  pendingActivations.set(data.projectKey, {
448
461
  agentKey: data.projectKey, agentName: projName, cwd: data.cwd,
@@ -450,7 +463,7 @@ function createCommandRouter(deps) {
450
463
  });
451
464
  }
452
465
  await bot.sendMessage(chatId,
453
- `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}\n\n` +
466
+ `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
454
467
  `**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
455
468
  );
456
469
  return true;
@@ -458,14 +471,15 @@ function createCommandRouter(deps) {
458
471
 
459
472
  if (wantsBind) {
460
473
  const agentName = deriveAgentName(input, workspaceDir);
461
- const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null);
474
+ const inferredEngine = inferAgentEngineFromText(input);
475
+ const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null, { engine: inferredEngine });
462
476
  if (!res.ok) {
463
477
  await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
464
478
  return true;
465
479
  }
466
480
  const data = res.data || {};
467
481
  const projName = projectNameFromResult(data, agentName);
468
- if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName);
482
+ if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName, (data.project && data.project.engine) || getDefaultEngine());
469
483
  await bot.sendMessage(chatId, `✅ 已绑定 Agent\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
470
484
  return true;
471
485
  }
@@ -504,8 +518,10 @@ function createCommandRouter(deps) {
504
518
  const proj = config.projects[mappedKey];
505
519
  const projCwd = normalizeCwd(proj.cwd);
506
520
  const cur = loadState().sessions?.[chatId];
507
- if (!cur || cur.cwd !== projCwd) {
508
- attachOrCreateSession(chatId, projCwd, proj.name || mappedKey);
521
+ const curEngine = String((cur && cur.engine) || getDefaultEngine()).toLowerCase();
522
+ const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
523
+ if (!cur || cur.cwd !== projCwd || curEngine !== projEngine) {
524
+ attachOrCreateSession(chatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
509
525
  }
510
526
  }
511
527
 
@@ -564,7 +580,7 @@ function createCommandRouter(deps) {
564
580
  '/undo <hash> — 回退到指定 git checkpoint',
565
581
  '/quit — 结束会话,重新加载 MCP/配置',
566
582
  '',
567
- `⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
583
+ `⚙️ /model [${currentModel}] /engine [${getDefaultEngine()}] /provider [${currentProvider}] /distill-model /status /tasks /run /budget /reload /mentor`,
568
584
  '🧩 /TeamTask create <agent> <目标> [--scope <id>] · /TeamTask · /TeamTask <id>',
569
585
  '🧠 /memory — 记忆统计 · /memory <关键词> — 搜索事实',
570
586
  '🧬 /skill-evo — 查看/处理技能演化队列',
@@ -604,7 +620,7 @@ function createCommandRouter(deps) {
604
620
  if (quickAgent && !quickAgent.rest) {
605
621
  const { key, proj } = quickAgent;
606
622
  const projCwd = normalizeCwd(proj.cwd);
607
- attachOrCreateSession(chatId, projCwd, proj.name || key);
623
+ attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
608
624
  log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
609
625
  await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
610
626
  return;
@@ -33,22 +33,31 @@ projects:
33
33
 
34
34
  heartbeat:
35
35
  tasks:
36
- # 认知蒸馏:有偏好信号才触发,4小时冷却,仅在用户闲置时执行
36
+ # 认知蒸馏:有偏好信号才触发,2小时冷却,仅在用户闲置时执行
37
37
  - name: cognitive-distill
38
38
  type: script
39
39
  command: node ~/.metame/distill.js
40
- interval: 4h
40
+ interval: 2h
41
41
  precondition: "test -s ~/.metame/raw_signals.jsonl"
42
42
  require_idle: true
43
43
  notify: false
44
44
  enabled: true
45
45
 
46
- # 记忆提取:扫描未分析 session,提取长期事实和会话标签,4小时冷却(与 cognitive-distill 对齐)
46
+ # 记忆提取:扫描未分析 session,提取长期事实和会话标签,4小时冷却
47
47
  - name: memory-extract
48
48
  type: script
49
49
  command: node ~/.metame/memory-extract.js
50
50
  interval: 4h
51
- timeout: 600
51
+ timeout: 1800
52
+ require_idle: true
53
+ notify: false
54
+ enabled: true
55
+
56
+ # 记忆垃圾回收:每天 02:00 清理过期/重复记忆
57
+ - name: memory-gc
58
+ type: script
59
+ command: node ~/.metame/memory-gc.js
60
+ at: "02:00"
52
61
  require_idle: true
53
62
  notify: false
54
63
  enabled: true
@@ -63,6 +72,15 @@ heartbeat:
63
72
  notify: false
64
73
  enabled: true
65
74
 
75
+ # 自我反思:每天 23:00 扫描纠错信号,提炼成长模式
76
+ - name: self-reflect
77
+ type: script
78
+ command: node ~/.metame/self-reflect.js
79
+ at: "23:00"
80
+ require_idle: true
81
+ notify: false
82
+ enabled: true
83
+
66
84
  # 夜间记忆蒸馏:每天 01:00 提炼热区事实为决策与经验文档
67
85
  - name: nightly-reflect
68
86
  type: script
@@ -134,3 +152,9 @@ daemon:
134
152
  # Mobile users can't click "allow" — so we pre-authorize everything.
135
153
  # Security relies on allowed_chat_ids whitelist, not tool restrictions.
136
154
  dangerously_skip_permissions: true
155
+ mentor:
156
+ enabled: false
157
+ friction_level: 3
158
+ mode: gentle
159
+ exclude_agents: [personal, xianyu]
160
+ emotion_keywords_extra: []
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execSync } = require('child_process');
7
+
8
+ const CODEX_TOOL_MAP = Object.freeze({
9
+ command_execution: 'Bash',
10
+ file_change: 'Write',
11
+ file_read: 'Read',
12
+ mcp_tool_call: 'MCP',
13
+ web_search: 'WebSearch',
14
+ web_fetch: 'WebFetch',
15
+ });
16
+
17
+ function normalizeEngineName(name) {
18
+ const text = String(name || '').trim().toLowerCase();
19
+ return text === 'codex' ? 'codex' : 'claude';
20
+ }
21
+
22
+ function resolveBinary(engineName, deps = {}) {
23
+ const engine = normalizeEngineName(engineName);
24
+ const home = deps.HOME || os.homedir();
25
+ const fsMod = deps.fs || fs;
26
+ const pathMod = deps.path || path;
27
+ const execSyncFn = deps.execSync || execSync;
28
+
29
+ const key = engine === 'codex' ? 'codex' : 'claude';
30
+ const cmd = process.platform === 'win32' ? `where ${key}` : `which ${key} 2>/dev/null`;
31
+ try {
32
+ const resolved = execSyncFn(cmd, { encoding: 'utf8', timeout: 3000 }).trim().split('\n')[0];
33
+ if (resolved) return resolved;
34
+ } catch { /* fallback */ }
35
+
36
+ const candidates = engine === 'codex'
37
+ ? [
38
+ pathMod.join(home, '.local', 'bin', 'codex'),
39
+ '/usr/local/bin/codex',
40
+ '/opt/homebrew/bin/codex',
41
+ ]
42
+ : [
43
+ pathMod.join(home, '.local', 'bin', 'claude'),
44
+ pathMod.join(home, '.npm-global', 'bin', 'claude'),
45
+ '/usr/local/bin/claude',
46
+ '/opt/homebrew/bin/claude',
47
+ ];
48
+ for (const p of candidates) {
49
+ if (fsMod.existsSync(p)) return p;
50
+ }
51
+ return key;
52
+ }
53
+
54
+ const ENGINE_DISTILL_MAP = Object.freeze({
55
+ claude: 'haiku',
56
+ codex: 'gpt-5.1-codex-mini',
57
+ });
58
+
59
+ function detectDefaultEngine(deps = {}) {
60
+ for (const engine of ['claude', 'codex']) {
61
+ const bin = resolveBinary(engine, deps);
62
+ if (bin !== engine) return engine; // resolveBinary found a real path
63
+ }
64
+ return 'claude'; // ultimate fallback
65
+ }
66
+
67
+ function classifyEngineError(text) {
68
+ const msg = String(text || '').trim();
69
+ if (!msg) return null;
70
+ if (/(auth|unauthorized|login|api key|authentication|permission denied|forbidden|401|403)/i.test(msg)) {
71
+ return {
72
+ code: 'AUTH_REQUIRED',
73
+ message: '认证失败,请先执行 `codex login`(或配置 OPENAI_API_KEY)后重试。',
74
+ };
75
+ }
76
+ if (/(rate.?limit|too many requests|quota|429)/i.test(msg)) {
77
+ return {
78
+ code: 'RATE_LIMIT',
79
+ message: '请求频率或配额受限,请稍后重试。',
80
+ };
81
+ }
82
+ return {
83
+ code: 'EXEC_FAILURE',
84
+ message: msg,
85
+ };
86
+ }
87
+
88
+ function parseJsonLine(line) {
89
+ try {
90
+ return JSON.parse(line);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function parseClaudeStreamEvent(line) {
97
+ const raw = parseJsonLine(line);
98
+ if (!raw || typeof raw !== 'object') return [];
99
+
100
+ const out = [];
101
+ if (raw.type === 'assistant' && raw.message && Array.isArray(raw.message.content)) {
102
+ for (const block of raw.message.content) {
103
+ if (!block) continue;
104
+ if (block.type === 'text' && block.text) {
105
+ out.push({ type: 'text', text: String(block.text), raw });
106
+ } else if (block.type === 'tool_use') {
107
+ out.push({
108
+ type: 'tool_use',
109
+ toolName: block.name || 'Tool',
110
+ toolInput: block.input || {},
111
+ raw,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ if (raw.type === 'result') {
117
+ if (raw.result) out.push({ type: 'text', text: String(raw.result), raw });
118
+ out.push({ type: 'done', usage: raw.usage || null, raw });
119
+ }
120
+ if (raw.type === 'content_block_start' || raw.type === 'content_block_delta') {
121
+ out.push({ type: 'tool_result', raw });
122
+ }
123
+ if (raw.type === 'error') {
124
+ const classified = classifyEngineError(raw.error || raw.message || '');
125
+ if (classified) out.push({ type: 'error', ...classified, raw });
126
+ }
127
+ return out;
128
+ }
129
+
130
+ function parseCodexStreamEvent(line) {
131
+ const raw = parseJsonLine(line);
132
+ if (!raw || typeof raw !== 'object') return [];
133
+
134
+ const out = [];
135
+ if (raw.type === 'thread.started' && raw.thread_id) {
136
+ out.push({ type: 'session', sessionId: String(raw.thread_id), raw });
137
+ }
138
+
139
+ if ((raw.type === 'item.started' || raw.type === 'item.completed') && raw.item && raw.item.type) {
140
+ const itemType = String(raw.item.type);
141
+ const mapped = CODEX_TOOL_MAP[itemType] || itemType;
142
+ if (mapped && mapped !== 'reasoning' && itemType !== 'agent_message') {
143
+ if (raw.type === 'item.started') {
144
+ out.push({
145
+ type: 'tool_use',
146
+ toolName: mapped,
147
+ toolInput: {
148
+ command: raw.item.command || '',
149
+ file_path: raw.item.path || raw.item.file_path || '',
150
+ },
151
+ raw,
152
+ });
153
+ } else {
154
+ out.push({ type: 'tool_result', toolName: mapped, raw });
155
+ }
156
+ }
157
+ if (raw.type === 'item.completed' && itemType === 'agent_message' && raw.item.text) {
158
+ out.push({ type: 'text', text: String(raw.item.text), raw });
159
+ }
160
+ }
161
+
162
+ if (raw.type === 'turn.completed') {
163
+ out.push({ type: 'done', usage: raw.usage || null, raw });
164
+ }
165
+ if (raw.type === 'error') {
166
+ const classified = classifyEngineError(raw.error || raw.message || '');
167
+ if (classified) out.push({ type: 'error', ...classified, raw });
168
+ }
169
+ return out;
170
+ }
171
+
172
+ function buildClaudeArgs(options = {}) {
173
+ const { model = 'opus', readOnly = false, daemonCfg = {}, session = {} } = options;
174
+ const args = ['-p', '--model', model];
175
+ if (readOnly) {
176
+ const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
177
+ for (const tool of readOnlyTools) args.push('--allowedTools', tool);
178
+ } else if (daemonCfg.dangerously_skip_permissions) {
179
+ args.push('--dangerously-skip-permissions');
180
+ } else {
181
+ for (const tool of (daemonCfg.session_allowed_tools || [])) args.push('--allowedTools', tool);
182
+ }
183
+
184
+ if (session.id === '__continue__') {
185
+ args.push('--continue');
186
+ } else if (session.started && session.id) {
187
+ args.push('--resume', session.id);
188
+ } else if (session.id) {
189
+ args.push('--session-id', session.id);
190
+ }
191
+ return args;
192
+ }
193
+
194
+ function buildCodexArgs(options = {}) {
195
+ const { model = 'gpt-5-codex', readOnly = false, daemonCfg = {}, session = {}, cwd } = options;
196
+ const args = (session && session.started && session.id && session.id !== '__continue__')
197
+ ? ['exec', 'resume', session.id]
198
+ : ['exec'];
199
+
200
+ args.push('--json', '--skip-git-repo-check');
201
+ if (model) args.push('-m', model);
202
+ if (cwd) args.push('-C', cwd);
203
+
204
+ if (readOnly) {
205
+ args.push('-s', 'read-only');
206
+ } else if (daemonCfg.dangerously_skip_permissions) {
207
+ args.push('--dangerously-bypass-approvals-and-sandbox');
208
+ } else {
209
+ args.push('--full-auto');
210
+ }
211
+
212
+ // "-" means prompt is read from stdin.
213
+ args.push('-');
214
+ return args;
215
+ }
216
+
217
+ function createEngineRuntimeFactory(deps = {}) {
218
+ const home = deps.HOME || os.homedir();
219
+ const claudeBin = deps.CLAUDE_BIN || resolveBinary('claude', { ...deps, HOME: home });
220
+ const codexBin = deps.CODEX_BIN || resolveBinary('codex', { ...deps, HOME: home });
221
+ const getActiveProviderEnv = typeof deps.getActiveProviderEnv === 'function'
222
+ ? deps.getActiveProviderEnv
223
+ : (() => ({}));
224
+
225
+ return function getEngineRuntime(engineName) {
226
+ const engine = normalizeEngineName(engineName);
227
+ if (engine === 'codex') {
228
+ return {
229
+ name: 'codex',
230
+ binary: codexBin,
231
+ defaultModel: 'gpt-5-codex',
232
+ stdinBehavior: 'write-and-close',
233
+ killSignal: 'SIGTERM',
234
+ timeouts: { idleMs: 10 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
235
+ buildArgs: buildCodexArgs,
236
+ buildEnv: ({ metameProject = '' } = {}) => ({ ...process.env, METAME_PROJECT: metameProject }),
237
+ parseStreamEvent: parseCodexStreamEvent,
238
+ classifyError: classifyEngineError,
239
+ };
240
+ }
241
+ return {
242
+ name: 'claude',
243
+ binary: claudeBin,
244
+ defaultModel: 'opus',
245
+ stdinBehavior: 'write-and-close',
246
+ killSignal: 'SIGTERM',
247
+ timeouts: { idleMs: 5 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
248
+ buildArgs: buildClaudeArgs,
249
+ buildEnv: ({ metameProject = '' } = {}) => ({
250
+ ...(() => {
251
+ const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject };
252
+ delete env.CLAUDECODE;
253
+ return env;
254
+ })(),
255
+ }),
256
+ parseStreamEvent: parseClaudeStreamEvent,
257
+ classifyError: classifyEngineError,
258
+ };
259
+ };
260
+ }
261
+
262
+ module.exports = {
263
+ createEngineRuntimeFactory,
264
+ normalizeEngineName,
265
+ resolveBinary,
266
+ detectDefaultEngine,
267
+ ENGINE_DISTILL_MAP,
268
+ _private: {
269
+ classifyEngineError,
270
+ parseClaudeStreamEvent,
271
+ parseCodexStreamEvent,
272
+ buildClaudeArgs,
273
+ buildCodexArgs,
274
+ },
275
+ };
@@ -209,8 +209,9 @@ function createExecCommandHandler(deps) {
209
209
  const proc = activeProcesses.get(chatId);
210
210
  if (proc && proc.child) {
211
211
  proc.aborted = true;
212
- try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
213
- await bot.sendMessage(chatId, '⏹ Stopping Claude...');
212
+ const signal = proc.killSignal || 'SIGTERM';
213
+ try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
214
+ await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
214
215
  } else {
215
216
  await bot.sendMessage(chatId, 'No active task to stop.');
216
217
  }
@@ -228,7 +229,8 @@ function createExecCommandHandler(deps) {
228
229
  const proc = activeProcesses.get(chatId);
229
230
  if (proc && proc.child) {
230
231
  proc.aborted = true;
231
- try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
232
+ const signal = proc.killSignal || 'SIGTERM';
233
+ try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
232
234
  }
233
235
  const session = getSession(chatId);
234
236
  const name = session ? getSessionName(session.id) : null;
@@ -244,6 +246,10 @@ function createExecCommandHandler(deps) {
244
246
  await bot.sendMessage(chatId, '❌ No active session to compact.');
245
247
  return true;
246
248
  }
249
+ if (String(session.engine || '').toLowerCase() === 'codex') {
250
+ await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
251
+ return true;
252
+ }
247
253
  await bot.sendMessage(chatId, '🗜 Compacting session...');
248
254
 
249
255
  // Step 1: Read conversation from JSONL (fast, no Claude needed)
@@ -316,7 +322,7 @@ function createExecCommandHandler(deps) {
316
322
  // Step 4: Create new session with the summary
317
323
  const model = daemonCfg.model || 'opus';
318
324
  const oldName = getSessionName(session.id);
319
- const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '');
325
+ const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '', session.engine || 'claude');
320
326
  const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
321
327
  if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
322
328
  const preamble = buildProfilePreamble();
@@ -58,7 +58,43 @@ function createNotifier(deps) {
58
58
  }
59
59
  }
60
60
 
61
- return { notify, notifyAdmin };
61
+ /**
62
+ * Send only to personal (non-agent-bound) chat IDs.
63
+ * Agent-bound group chats (those in chat_agent_map) are excluded.
64
+ * Falls back to fsIds[0] if no personal chats are found.
65
+ * Used for system notifications that should not spam every agent group.
66
+ */
67
+ async function notifyPersonal(message) {
68
+ const config = getConfig();
69
+ const { telegramBridge, feishuBridge } = getBridges();
70
+
71
+ if (feishuBridge && feishuBridge.bot) {
72
+ const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
73
+ const fsIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
74
+ // Personal chats = allowed IDs not bound to any agent
75
+ const personalIds = fsIds.filter(id => !chatAgentMap[id]);
76
+ const targetIds = personalIds.length > 0 ? personalIds : fsIds.slice(0, 1);
77
+ for (const chatId of targetIds) {
78
+ try { await feishuBridge.bot.sendMessage(chatId, message); } catch (e) {
79
+ log('ERROR', `Feishu personal notify failed ${chatId}: ${e.message}`);
80
+ }
81
+ }
82
+ }
83
+
84
+ if (telegramBridge && telegramBridge.bot) {
85
+ const tgAgentMap = (config.telegram && config.telegram.chat_agent_map) || {};
86
+ const tgIds = (config.telegram && config.telegram.allowed_chat_ids) || [];
87
+ const personalIds = tgIds.filter(id => !tgAgentMap[id]);
88
+ const targetIds = personalIds.length > 0 ? personalIds : tgIds.slice(0, 1);
89
+ for (const chatId of targetIds) {
90
+ try { await telegramBridge.bot.sendMarkdown(chatId, message); } catch (e) {
91
+ log('ERROR', `Telegram personal notify failed ${chatId}: ${e.message}`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return { notify, notifyAdmin, notifyPersonal };
62
98
  }
63
99
 
64
100
  module.exports = { createNotifier };
@@ -50,6 +50,7 @@ function setupRuntimeWatchers(deps) {
50
50
  log,
51
51
  notifyFn,
52
52
  adminNotifyFn,
53
+ notifyPersonalFn,
53
54
  activeProcesses,
54
55
  getConfig,
55
56
  setConfig,
@@ -68,7 +69,7 @@ function setupRuntimeWatchers(deps) {
68
69
  refreshLogMaxSize(newConfig);
69
70
  const timer = getHeartbeatTimer();
70
71
  if (timer) clearInterval(timer);
71
- setHeartbeatTimer(startHeartbeat(newConfig, notifyFn));
72
+ setHeartbeatTimer(startHeartbeat(newConfig, notifyFn, notifyPersonalFn));
72
73
  const { general, project } = getAllTasks(newConfig);
73
74
  const totalCount = general.length + project.length;
74
75
  log('INFO', `Config reloaded: ${totalCount} tasks (${project.length} in projects)`);