metame-cli 1.4.34 → 1.5.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.
Files changed (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. 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() {
@@ -154,8 +155,8 @@ function createCommandRouter(deps) {
154
155
  const explicit = extractAgentName(input);
155
156
  if (explicit) return explicit;
156
157
  if (workspaceDir) {
157
- const segs = workspaceDir.split('/').filter(Boolean);
158
- if (segs.length > 0) return segs[segs.length - 1];
158
+ const basename = workspaceDir.split(/[/\\]/).filter(Boolean).pop();
159
+ if (basename) return basename;
159
160
  }
160
161
  return 'workspace-agent';
161
162
  }
@@ -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);
@@ -414,6 +422,10 @@ function createCommandRouter(deps) {
414
422
  await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent。先说“给这个群绑定一个 Agent,目录是 ~/xxx”。');
415
423
  return true;
416
424
  }
425
+ // Lazy migration: ensure soul layer exists for agents created before this feature
426
+ if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
427
+ await agentTools.repairAgentSoul(bound.project.cwd).catch(() => {});
428
+ }
417
429
  const roleDelta = deriveRoleDelta(input);
418
430
  const res = await agentTools.editAgentRoleDefinition(bound.project.cwd, roleDelta);
419
431
  if (!res.ok) {
@@ -435,14 +447,19 @@ function createCommandRouter(deps) {
435
447
  }
436
448
  const agentName = deriveAgentName(input, workspaceDir);
437
449
  const roleDelta = deriveCreateRoleDelta(input);
450
+ const inferredEngine = inferAgentEngineFromText(input);
438
451
  // Always skip binding creating chat — new group activates via /activate
439
- const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, { skipChatBinding: true });
452
+ const res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDelta, chatId, {
453
+ skipChatBinding: true,
454
+ engine: inferredEngine,
455
+ });
440
456
  if (!res.ok) {
441
457
  await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
442
458
  return true;
443
459
  }
444
460
  const data = res.data || {};
445
461
  const projName = projectNameFromResult(data, agentName);
462
+ const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
446
463
  if (data.projectKey && pendingActivations) {
447
464
  pendingActivations.set(data.projectKey, {
448
465
  agentKey: data.projectKey, agentName: projName, cwd: data.cwd,
@@ -450,7 +467,7 @@ function createCommandRouter(deps) {
450
467
  });
451
468
  }
452
469
  await bot.sendMessage(chatId,
453
- `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}\n\n` +
470
+ `✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
454
471
  `**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
455
472
  );
456
473
  return true;
@@ -458,14 +475,15 @@ function createCommandRouter(deps) {
458
475
 
459
476
  if (wantsBind) {
460
477
  const agentName = deriveAgentName(input, workspaceDir);
461
- const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null);
478
+ const inferredEngine = inferAgentEngineFromText(input);
479
+ const res = await agentTools.bindAgentToChat(chatId, agentName, workspaceDir || null, { engine: inferredEngine });
462
480
  if (!res.ok) {
463
481
  await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
464
482
  return true;
465
483
  }
466
484
  const data = res.data || {};
467
485
  const projName = projectNameFromResult(data, agentName);
468
- if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName);
486
+ if (data.cwd) attachOrCreateSession(chatId, normalizeCwd(data.cwd), projName, (data.project && data.project.engine) || getDefaultEngine());
469
487
  await bot.sendMessage(chatId, `✅ 已绑定 Agent\n名称: ${projName}\n目录: ${data.cwd || '(未知)'}`);
470
488
  return true;
471
489
  }
@@ -504,8 +522,10 @@ function createCommandRouter(deps) {
504
522
  const proj = config.projects[mappedKey];
505
523
  const projCwd = normalizeCwd(proj.cwd);
506
524
  const cur = loadState().sessions?.[chatId];
507
- if (!cur || cur.cwd !== projCwd) {
508
- attachOrCreateSession(chatId, projCwd, proj.name || mappedKey);
525
+ const curEngine = String((cur && cur.engine) || getDefaultEngine()).toLowerCase();
526
+ const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
527
+ if (!cur || cur.cwd !== projCwd || curEngine !== projEngine) {
528
+ attachOrCreateSession(chatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
509
529
  }
510
530
  }
511
531
 
@@ -551,6 +571,7 @@ function createCommandRouter(deps) {
551
571
  '/agent edit — 编辑当前 Agent 角色',
552
572
  '/agent unbind — 解绑当前群',
553
573
  '/agent reset — 重置当前 Agent 角色',
574
+ '/agent soul [repair] — 查看/修复 Agent Soul 身份层',
554
575
  '',
555
576
  '📂 Session 管理:',
556
577
  '/new [path] [name] — 新建会话',
@@ -564,7 +585,7 @@ function createCommandRouter(deps) {
564
585
  '/undo <hash> — 回退到指定 git checkpoint',
565
586
  '/quit — 结束会话,重新加载 MCP/配置',
566
587
  '',
567
- `⚙️ /model [${currentModel}] /provider [${currentProvider}] /status /tasks /run /budget /reload`,
588
+ `⚙️ /model [${currentModel}] /engine [${getDefaultEngine()}] /provider [${currentProvider}] /distill-model /status /tasks /run /budget /reload /mentor`,
568
589
  '🧩 /TeamTask create <agent> <目标> [--scope <id>] · /TeamTask · /TeamTask <id>',
569
590
  '🧠 /memory — 记忆统计 · /memory <关键词> — 搜索事实',
570
591
  '🧬 /skill-evo — 查看/处理技能演化队列',
@@ -604,7 +625,7 @@ function createCommandRouter(deps) {
604
625
  if (quickAgent && !quickAgent.rest) {
605
626
  const { key, proj } = quickAgent;
606
627
  const projCwd = normalizeCwd(proj.cwd);
607
- attachOrCreateSession(chatId, projCwd, proj.name || key);
628
+ attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
608
629
  log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
609
630
  await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
610
631
  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,328 @@
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 lines = execSyncFn(cmd, { encoding: 'utf8', timeout: 3000, ...(process.platform === 'win32' ? { windowsHide: true } : {}) })
33
+ .split('\n').map(l => l.trim()).filter(Boolean);
34
+ // On Windows prefer .cmd wrapper (reliably executable by spawn)
35
+ const preferred = process.platform === 'win32'
36
+ ? (lines.find(l => l.toLowerCase().endsWith(`${key}.cmd`)) || lines[0])
37
+ : lines[0];
38
+ if (preferred) return preferred;
39
+ } catch { /* fallback */ }
40
+
41
+ const candidates = engine === 'codex'
42
+ ? [
43
+ pathMod.join(home, '.local', 'bin', 'codex'),
44
+ '/usr/local/bin/codex',
45
+ '/opt/homebrew/bin/codex',
46
+ ]
47
+ : [
48
+ pathMod.join(home, '.local', 'bin', 'claude'),
49
+ pathMod.join(home, '.npm-global', 'bin', 'claude'),
50
+ '/usr/local/bin/claude',
51
+ '/opt/homebrew/bin/claude',
52
+ ];
53
+ for (const p of candidates) {
54
+ if (fsMod.existsSync(p)) return p;
55
+ }
56
+ return key;
57
+ }
58
+
59
+ // Single source of truth for all per-engine model config.
60
+ // All other code should read from here — no scattered hardcodes.
61
+ const ENGINE_MODEL_CONFIG = Object.freeze({
62
+ claude: {
63
+ main: 'sonnet', // default session model
64
+ distill: 'haiku', // background/cheap tasks
65
+ options: [ // /model button list
66
+ { value: 'opus', label: 'opus · 最强' },
67
+ { value: 'sonnet', label: 'sonnet · 均衡' },
68
+ { value: 'haiku', label: 'haiku · 轻量' },
69
+ ],
70
+ provider: 'anthropic',
71
+ hint: null,
72
+ },
73
+ codex: {
74
+ main: 'gpt-5.4', // recommended for most tasks (official default)
75
+ distill: 'gpt-5.1-codex-mini', // cost-effective mini
76
+ options: [ // quick-pick buttons (official model names)
77
+ { value: 'gpt-5.4', label: 'gpt-5.4 · 推荐' },
78
+ { value: 'gpt-5.3-codex', label: 'gpt-5.3-codex · 最新 Codex 专用' },
79
+ { value: 'gpt-5.1-codex-max', label: 'gpt-5.1-codex-max · 长任务' },
80
+ { value: 'gpt-5.1-codex-mini', label: 'gpt-5.1-codex-mini · 轻量' },
81
+ ],
82
+ provider: 'openai',
83
+ hint: '或直接发送任意 OpenAI 模型名切换',
84
+ },
85
+ });
86
+
87
+ // Backward-compat aliases (derived, do not edit directly)
88
+ const ENGINE_DISTILL_MAP = Object.freeze(
89
+ Object.fromEntries(Object.entries(ENGINE_MODEL_CONFIG).map(([k, v]) => [k, v.distill]))
90
+ );
91
+ const ENGINE_DEFAULT_MODEL = Object.freeze(
92
+ Object.fromEntries(Object.entries(ENGINE_MODEL_CONFIG).map(([k, v]) => [k, v.main]))
93
+ );
94
+
95
+ function detectDefaultEngine(deps = {}) {
96
+ for (const engine of ['claude', 'codex']) {
97
+ const bin = resolveBinary(engine, deps);
98
+ if (bin !== engine) return engine; // resolveBinary found a real path
99
+ }
100
+ return 'claude'; // ultimate fallback
101
+ }
102
+
103
+ function classifyEngineError(text) {
104
+ const msg = String(text || '').trim();
105
+ if (!msg) return null;
106
+ if (/(auth|unauthorized|login|api key|authentication|permission denied|forbidden|401|403)/i.test(msg)) {
107
+ return {
108
+ code: 'AUTH_REQUIRED',
109
+ message: '认证失败,请先执行 `codex login`(或配置 OPENAI_API_KEY)后重试。',
110
+ };
111
+ }
112
+ if (/(rate.?limit|too many requests|quota|429)/i.test(msg)) {
113
+ return {
114
+ code: 'RATE_LIMIT',
115
+ message: '请求频率或配额受限,请稍后重试。',
116
+ };
117
+ }
118
+ return {
119
+ code: 'EXEC_FAILURE',
120
+ message: msg,
121
+ };
122
+ }
123
+
124
+ function parseJsonLine(line) {
125
+ try {
126
+ return JSON.parse(line);
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function parseClaudeStreamEvent(line) {
133
+ const raw = parseJsonLine(line);
134
+ if (!raw || typeof raw !== 'object') return [];
135
+
136
+ const out = [];
137
+ if (raw.type === 'assistant' && raw.message && Array.isArray(raw.message.content)) {
138
+ for (const block of raw.message.content) {
139
+ if (!block) continue;
140
+ if (block.type === 'text' && block.text) {
141
+ out.push({ type: 'text', text: String(block.text), raw });
142
+ } else if (block.type === 'tool_use') {
143
+ out.push({
144
+ type: 'tool_use',
145
+ toolName: block.name || 'Tool',
146
+ toolInput: block.input || {},
147
+ raw,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ if (raw.type === 'system' && raw.subtype === 'init' && raw.session_id) {
153
+ out.push({ type: 'session', sessionId: String(raw.session_id), raw });
154
+ }
155
+ if (raw.type === 'result') {
156
+ if (raw.session_id) out.push({ type: 'session', sessionId: String(raw.session_id), raw });
157
+ if (raw.result) out.push({ type: 'text', text: String(raw.result), raw });
158
+ out.push({ type: 'done', usage: raw.usage || null, raw });
159
+ }
160
+ if (raw.type === 'content_block_start' || raw.type === 'content_block_delta') {
161
+ out.push({ type: 'tool_result', raw });
162
+ }
163
+ if (raw.type === 'error') {
164
+ const classified = classifyEngineError(raw.error || raw.message || '');
165
+ if (classified) out.push({ type: 'error', ...classified, raw });
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function parseCodexStreamEvent(line) {
171
+ const raw = parseJsonLine(line);
172
+ if (!raw || typeof raw !== 'object') return [];
173
+
174
+ const out = [];
175
+ if (raw.type === 'thread.started' && raw.thread_id) {
176
+ out.push({ type: 'session', sessionId: String(raw.thread_id), raw });
177
+ }
178
+
179
+ if ((raw.type === 'item.started' || raw.type === 'item.completed') && raw.item && raw.item.type) {
180
+ const itemType = String(raw.item.type);
181
+ const mapped = CODEX_TOOL_MAP[itemType] || itemType;
182
+ if (mapped && mapped !== 'reasoning' && itemType !== 'agent_message') {
183
+ if (raw.type === 'item.started') {
184
+ out.push({
185
+ type: 'tool_use',
186
+ toolName: mapped,
187
+ toolInput: {
188
+ command: raw.item.command || '',
189
+ file_path: raw.item.path || raw.item.file_path || '',
190
+ },
191
+ raw,
192
+ });
193
+ } else {
194
+ out.push({ type: 'tool_result', toolName: mapped, raw });
195
+ }
196
+ }
197
+ if (raw.type === 'item.completed' && itemType === 'agent_message' && raw.item.text) {
198
+ out.push({ type: 'text', text: String(raw.item.text), raw });
199
+ }
200
+ }
201
+
202
+ if (raw.type === 'turn.completed') {
203
+ out.push({ type: 'done', usage: raw.usage || null, raw });
204
+ }
205
+ if (raw.type === 'error') {
206
+ const classified = classifyEngineError(raw.error || raw.message || '');
207
+ if (classified) out.push({ type: 'error', ...classified, raw });
208
+ }
209
+ return out;
210
+ }
211
+
212
+ function buildClaudeArgs(options = {}) {
213
+ const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, daemonCfg = {}, session = {} } = options;
214
+ const args = ['-p', '--model', model];
215
+ if (readOnly) {
216
+ const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
217
+ for (const tool of readOnlyTools) args.push('--allowedTools', tool);
218
+ } else {
219
+ // Always bypass permission prompts — desktop users run in trusted local context,
220
+ // mobile users cannot click dialogs. Security relies on allowed_chat_ids whitelist.
221
+ args.push('--dangerously-skip-permissions');
222
+ }
223
+
224
+ if (session.id === '__continue__') {
225
+ args.push('--continue');
226
+ } else if (session.started && session.id) {
227
+ args.push('--resume', session.id);
228
+ } else if (session.id) {
229
+ args.push('--session-id', session.id);
230
+ }
231
+ return args;
232
+ }
233
+
234
+ function buildCodexArgs(options = {}) {
235
+ const { model = ENGINE_MODEL_CONFIG.codex.main, readOnly = false, daemonCfg = {}, session = {}, cwd } = options;
236
+ const isResume = (session && session.started && session.id && session.id !== '__continue__');
237
+ const args = isResume
238
+ ? ['exec', 'resume', session.id]
239
+ : ['exec'];
240
+
241
+ args.push('--json', '--skip-git-repo-check');
242
+ if (model) args.push('-m', model);
243
+ // -C (cwd) is only supported on fresh exec, not resume
244
+ if (cwd && !isResume) args.push('-C', cwd);
245
+
246
+ // Permission flags are only valid on fresh exec, not resume.
247
+ // `codex exec resume` does not accept -s or --dangerously-bypass-approvals-and-sandbox.
248
+ if (!isResume) {
249
+ if (readOnly) {
250
+ args.push('-s', 'read-only');
251
+ } else {
252
+ // Mobile sessions: user cannot click permission dialogs.
253
+ // Security relies on allowed_chat_ids whitelist, not tool restrictions.
254
+ args.push('--dangerously-bypass-approvals-and-sandbox');
255
+ }
256
+ }
257
+
258
+ // "-" means prompt is read from stdin.
259
+ args.push('-');
260
+ return args;
261
+ }
262
+
263
+ function createEngineRuntimeFactory(deps = {}) {
264
+ const home = deps.HOME || os.homedir();
265
+ const claudeBin = deps.CLAUDE_BIN || resolveBinary('claude', { ...deps, HOME: home });
266
+ const codexBin = deps.CODEX_BIN || resolveBinary('codex', { ...deps, HOME: home });
267
+ const getActiveProviderEnv = typeof deps.getActiveProviderEnv === 'function'
268
+ ? deps.getActiveProviderEnv
269
+ : (() => ({}));
270
+
271
+ return function getEngineRuntime(engineName) {
272
+ const engine = normalizeEngineName(engineName);
273
+ if (engine === 'codex') {
274
+ return {
275
+ name: 'codex',
276
+ binary: codexBin,
277
+ defaultModel: ENGINE_MODEL_CONFIG.codex.main,
278
+ stdinBehavior: 'write-and-close',
279
+ killSignal: 'SIGTERM',
280
+ timeouts: { idleMs: 10 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
281
+ buildArgs: buildCodexArgs,
282
+ buildEnv: ({ metameProject = '' } = {}) => {
283
+ const env = { ...process.env, METAME_PROJECT: metameProject };
284
+ // Unset CODEX_HOME if it points to a non-existent path (corrupted env var)
285
+ if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
286
+ return env;
287
+ },
288
+ parseStreamEvent: parseCodexStreamEvent,
289
+ classifyError: classifyEngineError,
290
+ };
291
+ }
292
+ return {
293
+ name: 'claude',
294
+ binary: claudeBin,
295
+ defaultModel: ENGINE_MODEL_CONFIG.claude.main,
296
+ stdinBehavior: 'write-and-close',
297
+ killSignal: 'SIGTERM',
298
+ timeouts: { idleMs: 5 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
299
+ buildArgs: buildClaudeArgs,
300
+ buildEnv: ({ metameProject = '' } = {}) => ({
301
+ ...(() => {
302
+ const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject };
303
+ delete env.CLAUDECODE;
304
+ return env;
305
+ })(),
306
+ }),
307
+ parseStreamEvent: parseClaudeStreamEvent,
308
+ classifyError: classifyEngineError,
309
+ };
310
+ };
311
+ }
312
+
313
+ module.exports = {
314
+ createEngineRuntimeFactory,
315
+ normalizeEngineName,
316
+ resolveBinary,
317
+ detectDefaultEngine,
318
+ ENGINE_MODEL_CONFIG,
319
+ ENGINE_DISTILL_MAP,
320
+ ENGINE_DEFAULT_MODEL,
321
+ _private: {
322
+ classifyEngineError,
323
+ parseClaudeStreamEvent,
324
+ parseCodexStreamEvent,
325
+ buildClaudeArgs,
326
+ buildCodexArgs,
327
+ },
328
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { classifyTaskUsage } = require('./usage-classifier');
4
+ const { normalizeModel } = require('./daemon-task-scheduler');
4
5
 
5
6
  function createExecCommandHandler(deps) {
6
7
  const {
@@ -23,6 +24,7 @@ function createExecCommandHandler(deps) {
23
24
  createSession,
24
25
  findSessionFile,
25
26
  loadConfig,
27
+ getDistillModel,
26
28
  } = deps;
27
29
 
28
30
  function truncateOutput(output, maxLen = 4000) {
@@ -178,7 +180,7 @@ function createExecCommandHandler(deps) {
178
180
  let taskPrompt = task.prompt;
179
181
  if (precheck.context) taskPrompt += `\n\n以下是相关原始数据:\n\`\`\`\n${precheck.context}\n\`\`\``;
180
182
  const fullPrompt = preamble + taskPrompt;
181
- const model = task.model || 'haiku';
183
+ const model = normalizeModel(task.model || getDistillModel());
182
184
  const claudeArgs = ['-p', '--model', model, '--dangerously-skip-permissions'];
183
185
  for (const t of (task.allowedTools || [])) claudeArgs.push('--allowedTools', t);
184
186
 
@@ -209,8 +211,9 @@ function createExecCommandHandler(deps) {
209
211
  const proc = activeProcesses.get(chatId);
210
212
  if (proc && proc.child) {
211
213
  proc.aborted = true;
212
- try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
213
- await bot.sendMessage(chatId, '⏹ Stopping Claude...');
214
+ const signal = proc.killSignal || 'SIGTERM';
215
+ try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
216
+ await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
214
217
  } else {
215
218
  await bot.sendMessage(chatId, 'No active task to stop.');
216
219
  }
@@ -228,7 +231,8 @@ function createExecCommandHandler(deps) {
228
231
  const proc = activeProcesses.get(chatId);
229
232
  if (proc && proc.child) {
230
233
  proc.aborted = true;
231
- try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
234
+ const signal = proc.killSignal || 'SIGTERM';
235
+ try { process.kill(-proc.child.pid, signal); } catch { proc.child.kill(signal); }
232
236
  }
233
237
  const session = getSession(chatId);
234
238
  const name = session ? getSessionName(session.id) : null;
@@ -244,6 +248,10 @@ function createExecCommandHandler(deps) {
244
248
  await bot.sendMessage(chatId, '❌ No active session to compact.');
245
249
  return true;
246
250
  }
251
+ if (String(session.engine || '').toLowerCase() === 'codex') {
252
+ await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
253
+ return true;
254
+ }
247
255
  await bot.sendMessage(chatId, '🗜 Compacting session...');
248
256
 
249
257
  // Step 1: Read conversation from JSONL (fast, no Claude needed)
@@ -298,9 +306,9 @@ function createExecCommandHandler(deps) {
298
306
  digest = entry + digest;
299
307
  }
300
308
 
301
- // Step 3: Summarize with haiku (new process, no --resume, fast)
309
+ // Step 3: Summarize with distill model (new process, no --resume, fast)
302
310
  const daemonCfg = loadConfig().daemon || {};
303
- const compactArgs = ['-p', '--model', 'haiku', '--no-session-persistence'];
311
+ const compactArgs = ['-p', '--model', getDistillModel(), '--no-session-persistence'];
304
312
  if (daemonCfg.dangerously_skip_permissions) compactArgs.push('--dangerously-skip-permissions');
305
313
  const { output, error } = await spawnClaudeAsync(
306
314
  compactArgs,
@@ -316,7 +324,7 @@ function createExecCommandHandler(deps) {
316
324
  // Step 4: Create new session with the summary
317
325
  const model = daemonCfg.model || 'opus';
318
326
  const oldName = getSessionName(session.id);
319
- const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '');
327
+ const newSession = createSession(chatId, session.cwd, oldName ? oldName + ' (compacted)' : '', session.engine || 'claude');
320
328
  const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
321
329
  if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
322
330
  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 };