metame-cli 1.5.4 → 1.5.6

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 +6 -1
  2. package/index.js +277 -55
  3. package/package.json +3 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +18 -6
  6. package/scripts/bin/push-clean.sh +72 -0
  7. package/scripts/daemon-admin-commands.js +266 -64
  8. package/scripts/daemon-agent-commands.js +188 -66
  9. package/scripts/daemon-bridges.js +475 -50
  10. package/scripts/daemon-checkpoints.js +84 -30
  11. package/scripts/daemon-claude-engine.js +651 -103
  12. package/scripts/daemon-command-router.js +134 -27
  13. package/scripts/daemon-command-session-route.js +118 -0
  14. package/scripts/daemon-default.yaml +2 -0
  15. package/scripts/daemon-dispatch-cards.js +185 -0
  16. package/scripts/daemon-engine-runtime.js +96 -20
  17. package/scripts/daemon-exec-commands.js +106 -50
  18. package/scripts/daemon-file-browser.js +63 -7
  19. package/scripts/daemon-notify.js +18 -4
  20. package/scripts/daemon-ops-commands.js +28 -6
  21. package/scripts/daemon-remote-dispatch.js +34 -2
  22. package/scripts/daemon-session-commands.js +102 -45
  23. package/scripts/daemon-session-store.js +497 -66
  24. package/scripts/daemon-siri-bridge.js +234 -0
  25. package/scripts/daemon-siri-imessage.js +209 -0
  26. package/scripts/daemon-task-scheduler.js +10 -2
  27. package/scripts/{team-dispatch.js → daemon-team-dispatch.js} +150 -11
  28. package/scripts/daemon.js +484 -181
  29. package/scripts/docs/hook-config.md +7 -4
  30. package/scripts/docs/maintenance-manual.md +10 -3
  31. package/scripts/docs/pointer-map.md +2 -2
  32. package/scripts/feishu-adapter.js +7 -15
  33. package/scripts/hooks/doc-router.js +29 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +9 -40
  36. package/scripts/intent-registry.js +59 -0
  37. package/scripts/memory-extract.js +59 -0
  38. package/scripts/mentor-engine.js +6 -0
  39. package/scripts/schema.js +1 -0
  40. package/scripts/self-reflect.js +110 -12
  41. package/scripts/session-analytics.js +160 -0
  42. package/scripts/signal-capture.js +1 -1
  43. package/scripts/hooks/intent-agent-manage.js +0 -50
  44. package/scripts/hooks/intent-hook-config.js +0 -28
@@ -7,8 +7,14 @@ const {
7
7
  } = require('./usage-classifier');
8
8
  const { IS_WIN } = require('./platform');
9
9
  const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
10
- const { resolveProjectKey: _resolveProjectKey } = require('./team-dispatch');
11
- const { parseRemoteTargetRef, normalizeRemoteDispatchConfig } = require('./daemon-remote-dispatch');
10
+ const { resolveProjectKey: _resolveProjectKey } = require('./daemon-team-dispatch');
11
+ const {
12
+ parseRemoteTargetRef,
13
+ getRemoteDispatchStatus,
14
+ generatePairCode,
15
+ isValidPairCode,
16
+ deriveSecretFromPairCode,
17
+ } = require('./daemon-remote-dispatch');
12
18
  let mentorEngine = null;
13
19
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
14
20
 
@@ -41,7 +47,7 @@ function createAdminCommandHandler(deps) {
41
47
  getDistillModel = () => 'haiku',
42
48
  } = deps;
43
49
 
44
- // resolveProjectKey: imported from team-dispatch.js (shared with dispatch_to and daemon.js)
50
+ // resolveProjectKey: imported from daemon-team-dispatch.js (shared with dispatch_to and daemon.js)
45
51
  const resolveProjectKey = _resolveProjectKey;
46
52
 
47
53
  /**
@@ -71,6 +77,14 @@ function createAdminCommandHandler(deps) {
71
77
  return map[String(chatId)] || 'user';
72
78
  }
73
79
 
80
+ function resolveBoundProjectKey(chatId, config) {
81
+ const map = {
82
+ ...(config && config.feishu ? config.feishu.chat_agent_map : {}),
83
+ ...(config && config.telegram ? config.telegram.chat_agent_map : {}),
84
+ };
85
+ return map[String(chatId)] || '';
86
+ }
87
+
74
88
  function popFlag(input, flagName) {
75
89
  const src = String(input || '');
76
90
  const re = new RegExp(`(?:^|\\s)--${flagName}\\s+(\\S+)`, 'i');
@@ -99,6 +113,87 @@ function createAdminCommandHandler(deps) {
99
113
  };
100
114
  }
101
115
 
116
+ function isLikelyTeamTaskResumeIntent(text) {
117
+ const src = String(text || '').trim();
118
+ if (!src || src.startsWith('/')) return false;
119
+ if (src.length < 4 || src.length > 120) return false;
120
+ if (/(?:新建|创建|查看|列出|列表|详情|状态|有哪些|teamtask\s*$|\/teamtask)/i.test(src)) return false;
121
+ return /(?:继续(?:做|改|修)?|接着(?:做|改|修)?|续上|接续|返工|复工|再修(?:一下)?|再改(?:一下)?).*(?:上次|那个|这单|这个任务|任务|TeamTask|team task|工单)?|(?:上次|那个|这单|这个任务).*(?:继续|接着|返工|复工|再修|再改)/i.test(src);
122
+ }
123
+
124
+ function listAutoResumeCandidates(chatId, senderKey, config) {
125
+ if (!taskBoard || typeof taskBoard.listRecentTasks !== 'function') return [];
126
+ const now = Date.now();
127
+ const chatKey = String(chatId);
128
+ const recent = taskBoard.listRecentTasks(12, null, 'team');
129
+ return recent.filter((task) => {
130
+ if (!task || task.task_kind !== 'team') return false;
131
+ if (!config.projects || !config.projects[task.to_agent]) return false;
132
+ const sourceChatId = String(task.inputs && task.inputs.source_chat_id || '').trim();
133
+ if (!sourceChatId || sourceChatId !== chatKey) return false;
134
+ const updatedAt = Date.parse(task.updated_at || task.created_at || '');
135
+ if (!Number.isFinite(updatedAt) || (now - updatedAt) > 12 * 3600_000) return false;
136
+ const participants = Array.isArray(task.participants) ? task.participants : [];
137
+ return task.from_agent === senderKey || participants.includes(senderKey);
138
+ });
139
+ }
140
+
141
+ function buildTeamTaskResumeEnvelope(task, targetKey, chatId, config) {
142
+ return taskEnvelope && taskEnvelope.normalizeTaskEnvelope
143
+ ? taskEnvelope.normalizeTaskEnvelope({
144
+ ...task,
145
+ status: 'queued',
146
+ updated_at: new Date().toISOString(),
147
+ task_kind: 'team',
148
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
149
+ }, {
150
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
151
+ to_agent: targetKey,
152
+ scope_id: task.scope_id || task.task_id,
153
+ })
154
+ : {
155
+ task_id: task.task_id,
156
+ scope_id: task.scope_id || task.task_id,
157
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
158
+ to_agent: targetKey,
159
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
160
+ goal: task.goal,
161
+ definition_of_done: task.definition_of_done || [],
162
+ inputs: task.inputs || {},
163
+ artifacts: task.artifacts || [],
164
+ owned_paths: task.owned_paths || [],
165
+ priority: task.priority || 'normal',
166
+ status: 'queued',
167
+ task_kind: 'team',
168
+ created_at: task.created_at,
169
+ updated_at: new Date().toISOString(),
170
+ };
171
+ }
172
+
173
+ function dispatchTeamTaskResume(task, chatId, config, senderId = null) {
174
+ const targetKey = task.to_agent;
175
+ if (!config.projects || !config.projects[targetKey]) {
176
+ return { success: false, error: `target_missing:${targetKey}` };
177
+ }
178
+ const envelope = buildTeamTaskResumeEnvelope(task, targetKey, chatId, config);
179
+ const result = dispatchTask(targetKey, {
180
+ from: envelope.from_agent || 'user',
181
+ type: 'task',
182
+ priority: envelope.priority || 'normal',
183
+ payload: {
184
+ title: envelope.goal.slice(0, 60),
185
+ prompt: envelope.goal,
186
+ task_envelope: envelope,
187
+ },
188
+ callback: false,
189
+ new_session: false,
190
+ source_chat_id: String(chatId),
191
+ source_sender_key: envelope.from_agent || resolveSenderKey(chatId, config),
192
+ source_sender_id: String(senderId || '').trim() || '',
193
+ }, config);
194
+ return { success: !!(result && result.success), result, envelope, targetKey };
195
+ }
196
+
102
197
  function formatTaskSchedule(task) {
103
198
  const at = typeof task.at === 'string' ? task.at.trim() : '';
104
199
  if (at) {
@@ -167,8 +262,30 @@ function createAdminCommandHandler(deps) {
167
262
  }
168
263
  }
169
264
 
265
+ async function sendLocalDispatchReceipt(bot, chatId, targetKey, projInfo, result, preview) {
266
+ if (!result || !result.success) return;
267
+ const icon = projInfo && projInfo.icon ? projInfo.icon : '🤖';
268
+ const name = projInfo && projInfo.name ? projInfo.name : targetKey;
269
+ const lines = [
270
+ '📮 Dispatch 回执',
271
+ '',
272
+ `状态: ${icon} ${name} 已接收并入队`,
273
+ ];
274
+ if (result.id) lines.push(`编号: ${result.id}`);
275
+ if (preview) lines.push(`摘要: ${String(preview).slice(0, 120)}`);
276
+ if (result.task_id) {
277
+ lines.push('');
278
+ lines.push(`TeamTask: ${result.task_id}`);
279
+ if (result.scope_id && result.scope_id !== result.task_id) {
280
+ lines.push(`Scope: ${result.scope_id}`);
281
+ }
282
+ lines.push(`如需复工,请使用: /TeamTask resume ${result.task_id}`);
283
+ }
284
+ await bot.sendMessage(chatId, lines.join('\n'));
285
+ }
286
+
170
287
  async function handleAdminCommand(ctx) {
171
- const { bot, chatId, text } = ctx;
288
+ const { bot, chatId, text, senderId = null } = ctx;
172
289
  const state = ctx.state || {};
173
290
  let config = ctx.config || {};
174
291
 
@@ -368,6 +485,39 @@ function createAdminCommandHandler(deps) {
368
485
  return { handled: true, config };
369
486
  }
370
487
 
488
+ if (isLikelyTeamTaskResumeIntent(text)) {
489
+ const senderKey = resolveSenderKey(chatId, config);
490
+ const candidates = listAutoResumeCandidates(chatId, senderKey, config);
491
+ if (candidates.length === 1) {
492
+ const task = candidates[0];
493
+ const resumed = dispatchTeamTaskResume(task, chatId, config, senderId);
494
+ if (resumed.success) {
495
+ if (taskBoard && typeof taskBoard.appendTaskEvent === 'function') {
496
+ taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId), source: 'nl_auto_resume' });
497
+ }
498
+ await bot.sendMessage(chatId, [
499
+ `🔄 已自动续跑最近的 TeamTask: ${task.task_id}`,
500
+ `目标: ${task.to_agent}`,
501
+ `意图: ${text.trim().slice(0, 80)}`,
502
+ '回执会在目标端真正接收后返回。',
503
+ ].join('\n'));
504
+ await sendLocalDispatchReceipt(bot, chatId, resumed.targetKey, config.projects[resumed.targetKey], resumed.result, resumed.envelope.goal);
505
+ return { handled: true, config };
506
+ }
507
+ await bot.sendMessage(chatId, `❌ 自动续跑失败: ${resumed.result && resumed.result.error ? resumed.result.error : 'unknown_error'}`);
508
+ return { handled: true, config };
509
+ }
510
+ if (candidates.length > 1) {
511
+ const lines = ['⚠️ 检测到你可能想复工 TeamTask,但最近有多条候选任务:'];
512
+ for (const task of candidates.slice(0, 3)) {
513
+ lines.push(`- ${task.task_id} [${task.status}] ${task.goal.slice(0, 50)}`);
514
+ }
515
+ lines.push('请直接回复更明确一点,或使用 /TeamTask 查看后再选择。');
516
+ await bot.sendMessage(chatId, lines.join('\n'));
517
+ return { handled: true, config };
518
+ }
519
+ }
520
+
371
521
  // /TeamTask — create/list/detail/resume team collaboration tasks
372
522
  const teamTaskCmdMatch = text.match(/^\/teamtask(?:\s+([\s\S]+))?$/i);
373
523
  if (teamTaskCmdMatch) {
@@ -432,13 +582,18 @@ function createAdminCommandHandler(deps) {
432
582
  task_envelope: envelope,
433
583
  },
434
584
  callback: false,
585
+ source_chat_id: String(chatId),
586
+ source_sender_key: senderKey,
587
+ source_sender_id: String(senderId || '').trim() || '',
435
588
  }, config);
436
589
  if (result.success) {
437
590
  await bot.sendMessage(chatId, [
438
- `✅ 已创建 TeamTask 并派发: ${envelope.task_id}`,
591
+ `✅ 已创建 TeamTask 并提交派发: ${envelope.task_id}`,
439
592
  `Scope: ${envelope.scope_id || envelope.task_id}`,
593
+ '回执会在目标端真正接收后返回。',
440
594
  `查看: /TeamTask ${envelope.task_id}`,
441
595
  ].join('\n'));
596
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, config.projects[targetKey], result, goal);
442
597
  } else {
443
598
  await bot.sendMessage(chatId, `❌ 创建 TeamTask 失败: ${result.error}`);
444
599
  }
@@ -478,52 +633,13 @@ function createAdminCommandHandler(deps) {
478
633
  await bot.sendMessage(chatId, `❌ 目标 agent 不存在: ${targetKey}`);
479
634
  return { handled: true, config };
480
635
  }
481
- const envelope = taskEnvelope && taskEnvelope.normalizeTaskEnvelope
482
- ? taskEnvelope.normalizeTaskEnvelope({
483
- ...task,
484
- status: 'queued',
485
- updated_at: new Date().toISOString(),
486
- task_kind: 'team',
487
- participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
488
- }, {
489
- from_agent: task.from_agent || resolveSenderKey(chatId, config),
490
- to_agent: targetKey,
491
- scope_id: task.scope_id || task.task_id,
492
- })
493
- : {
494
- task_id: task.task_id,
495
- scope_id: task.scope_id || task.task_id,
496
- from_agent: task.from_agent || resolveSenderKey(chatId, config),
497
- to_agent: targetKey,
498
- participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
499
- goal: task.goal,
500
- definition_of_done: task.definition_of_done || [],
501
- inputs: task.inputs || {},
502
- artifacts: task.artifacts || [],
503
- owned_paths: task.owned_paths || [],
504
- priority: task.priority || 'normal',
505
- status: 'queued',
506
- task_kind: 'team',
507
- created_at: task.created_at,
508
- updated_at: new Date().toISOString(),
509
- };
510
-
511
- const result = dispatchTask(targetKey, {
512
- from: envelope.from_agent || 'user',
513
- type: 'task',
514
- priority: envelope.priority || 'normal',
515
- payload: {
516
- title: envelope.goal.slice(0, 60),
517
- prompt: envelope.goal,
518
- task_envelope: envelope,
519
- },
520
- callback: false,
521
- new_session: false,
522
- }, config);
636
+ const resumed = dispatchTeamTaskResume(task, chatId, config, senderId);
637
+ const { result, envelope } = resumed;
523
638
 
524
639
  if (result.success) {
525
640
  taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId) });
526
- await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}`);
641
+ await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}\n回执会在目标端真正接收后返回。`);
642
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, config.projects[targetKey], result, envelope.goal);
527
643
  } else {
528
644
  await bot.sendMessage(chatId, `❌ 续跑失败: ${result.error}`);
529
645
  }
@@ -626,12 +742,12 @@ function createAdminCommandHandler(deps) {
626
742
 
627
743
  // /dispatch peers — show remote dispatch config
628
744
  if (args === 'peers') {
629
- const rd = normalizeRemoteDispatchConfig(config);
745
+ const rd = getRemoteDispatchStatus(config);
630
746
  if (!rd) {
631
747
  await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
632
748
  return { handled: true, config };
633
749
  }
634
- let msg = `📡 远端 Dispatch 配置\n─────────────\nself: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\n\n远端成员:\n`;
750
+ let msg = `📡 远端 Dispatch 配置\n─────────────\nself: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\nmode: pair code\nsecret: ${rd.hasSecret ? 'configured' : 'missing'}\n\n远端成员:\n`;
635
751
  let hasRemote = false;
636
752
  for (const [key, proj] of Object.entries(config.projects || {})) {
637
753
  if (!Array.isArray(proj.team)) continue;
@@ -647,6 +763,49 @@ function createAdminCommandHandler(deps) {
647
763
  return { handled: true, config };
648
764
  }
649
765
 
766
+ if (args === 'code') {
767
+ const rd = getRemoteDispatchStatus(config);
768
+ if (!rd) {
769
+ await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
770
+ return { handled: true, config };
771
+ }
772
+ const code = generatePairCode();
773
+ const secret = deriveSecretFromPairCode(code, rd.chatId);
774
+ backupConfig();
775
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
776
+ if (!cfg.feishu) cfg.feishu = {};
777
+ if (!cfg.feishu.remote_dispatch) cfg.feishu.remote_dispatch = {};
778
+ cfg.feishu.remote_dispatch.secret = secret;
779
+ writeConfigSafe(cfg);
780
+ config = loadConfig();
781
+ await bot.sendMessage(chatId, `🔐 配对码已生成\n\n配对码: ${code}\n\n把这 6 位码发到另一台设备执行:\n/dispatch pair ${code}`);
782
+ return { handled: true, config };
783
+ }
784
+
785
+ const pairMatch = args.match(/^pair\s+(\d{6})$/);
786
+ if (pairMatch) {
787
+ const rd = getRemoteDispatchStatus(config);
788
+ if (!rd) {
789
+ await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
790
+ return { handled: true, config };
791
+ }
792
+ const code = pairMatch[1];
793
+ if (!isValidPairCode(code)) {
794
+ await bot.sendMessage(chatId, '❌ 配对码必须是 6 位数字');
795
+ return { handled: true, config };
796
+ }
797
+ const secret = deriveSecretFromPairCode(code, rd.chatId);
798
+ backupConfig();
799
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
800
+ if (!cfg.feishu) cfg.feishu = {};
801
+ if (!cfg.feishu.remote_dispatch) cfg.feishu.remote_dispatch = {};
802
+ cfg.feishu.remote_dispatch.secret = secret;
803
+ writeConfigSafe(cfg);
804
+ config = loadConfig();
805
+ await bot.sendMessage(chatId, `✅ 配对码已写入\n\n当前设备: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\n现在可以测试 /dispatch to <peer:project> ...`);
806
+ return { handled: true, config };
807
+ }
808
+
650
809
  // /dispatch to <agent> <prompt>
651
810
  const toMatch = args.match(/^to\s+(\S+)\s+(.+)$/s);
652
811
  if (toMatch) {
@@ -664,6 +823,7 @@ function createAdminCommandHandler(deps) {
664
823
  prompt,
665
824
  source_chat_id: String(chatId),
666
825
  source_sender_key: senderKey,
826
+ source_sender_id: String(senderId || '').trim() || '',
667
827
  }, config);
668
828
  if (res.success) {
669
829
  await bot.sendMessage(chatId, `📡 已发送给 ${remoteTarget.peer}:${remoteTarget.project}`);
@@ -690,6 +850,7 @@ function createAdminCommandHandler(deps) {
690
850
  prompt,
691
851
  source_chat_id: String(chatId),
692
852
  source_sender_key: senderKey,
853
+ source_sender_id: String(senderId || '').trim() || '',
693
854
  }, config);
694
855
  if (res.success) {
695
856
  await bot.sendMessage(chatId, `📡 已发送给 ${projInfo.icon || '🤖'} ${projInfo.name || targetKey} (${projInfo.peer})`);
@@ -719,10 +880,14 @@ function createAdminCommandHandler(deps) {
719
880
  priority: 'normal',
720
881
  payload: { title: prompt.slice(0, 60), prompt },
721
882
  callback: false,
883
+ source_chat_id: String(chatId),
884
+ source_sender_key: senderKey,
885
+ source_sender_id: String(senderId || '').trim() || '',
722
886
  }, config, replyFn, dispatchStreamOptions);
723
887
 
724
888
  if (result.success) {
725
- await bot.sendMessage(chatId, `✅ 已派发给 ${projInfo.name || targetName},执行中…`);
889
+ await bot.sendMessage(chatId, `✅ 已提交派发给 ${projInfo.name || targetName},等待回执…`);
890
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, projInfo, result, prompt);
726
891
  } else {
727
892
  await bot.sendMessage(chatId, `❌ 派发失败: ${result.error}`);
728
893
  }
@@ -734,6 +899,8 @@ function createAdminCommandHandler(deps) {
734
899
  '/dispatch status — 查看状态',
735
900
  '/dispatch log — 查看记录',
736
901
  '/dispatch peers — 查看远端配置',
902
+ '/dispatch code — 生成 6 位配对码并写入本机',
903
+ '/dispatch pair <123456> — 输入 6 位配对码写入本机',
737
904
  '/dispatch to <agent> <任务内容> — 直接跨 agent 派发',
738
905
  '/dispatch to <peer:project> <任务内容> — 跨设备派发',
739
906
  '/TeamTask create <agent> <目标> [--scope <id>] [--parent <id>] — 创建/续接 TeamTask',
@@ -769,10 +936,14 @@ function createAdminCommandHandler(deps) {
769
936
  priority: 'normal',
770
937
  payload: { title: 'team message', prompt: `[来自团队的消息]\n\n${message}` },
771
938
  callback: false,
939
+ source_chat_id: String(chatId),
940
+ source_sender_key: senderKey,
941
+ source_sender_id: String(senderId || '').trim() || '',
772
942
  }, config, null, null);
773
943
 
774
944
  if (result.success) {
775
945
  await bot.sendMessage(chatId, `📬 已发送消息给 ${toProj.icon || '🤖'} ${toProj.name || targetKey}`);
946
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, toProj, result, message);
776
947
  } else {
777
948
  await bot.sendMessage(chatId, `❌ 发送失败: ${result.error}`);
778
949
  }
@@ -963,7 +1134,7 @@ function createAdminCommandHandler(deps) {
963
1134
  const arg = text.slice('/mentor'.length).trim();
964
1135
 
965
1136
  if (!arg || arg === 'status') {
966
- const status = mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
1137
+ const status = mentorCfg.enabled && mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
967
1138
  ? mentorEngine.getRuntimeStatus()
968
1139
  : { debt_count: 0, cooldown_remaining_ms: 0 };
969
1140
  const mode = String(mentorCfg.mode || modeFromLevel(mentorCfg.friction_level));
@@ -983,6 +1154,9 @@ function createAdminCommandHandler(deps) {
983
1154
 
984
1155
  if (arg === 'on' || arg === 'off') {
985
1156
  mentorCfg.enabled = arg === 'on';
1157
+ if (!mentorCfg.enabled && mentorEngine && typeof mentorEngine.clearRuntime === 'function') {
1158
+ mentorEngine.clearRuntime();
1159
+ }
986
1160
  writeConfigSafe(cfg);
987
1161
  config = loadConfig();
988
1162
  await bot.sendMessage(chatId, mentorCfg.enabled
@@ -1285,21 +1459,30 @@ function createAdminCommandHandler(deps) {
1285
1459
  // Switching engine auto-syncs: distill model + preferred provider (if available)
1286
1460
  if (text === '/engine' || text.startsWith('/engine ')) {
1287
1461
  const arg = text.slice('/engine'.length).trim().toLowerCase();
1462
+ const boundProjectKey = resolveBoundProjectKey(chatId, config);
1463
+ const boundProject = boundProjectKey && config && config.projects ? config.projects[boundProjectKey] : null;
1288
1464
  if (!arg) {
1289
- const cur = getDefaultEngine();
1290
- const curEngineCfg = ENGINE_MODEL_CONFIG[cur] || ENGINE_MODEL_CONFIG.claude;
1291
- const activeProvider = (cur === 'claude' && providerMod)
1465
+ const cur = boundProject && boundProject.engine ? String(boundProject.engine).trim().toLowerCase() : getDefaultEngine();
1466
+ const safeCur = cur === 'codex' ? 'codex' : 'claude';
1467
+ const curEngineCfg = ENGINE_MODEL_CONFIG[safeCur] || ENGINE_MODEL_CONFIG.claude;
1468
+ const activeProvider = (safeCur === 'claude' && providerMod)
1292
1469
  ? providerMod.getActiveName()
1293
1470
  : curEngineCfg.provider;
1294
1471
  const distill = getDistillModel();
1295
1472
  const daemonCfg = config.daemon || {};
1296
- const currentModel = resolveEngineModel(cur, daemonCfg);
1473
+ const currentModel = resolveEngineModel(safeCur, daemonCfg, boundProject && boundProject.model);
1474
+ const scopeLine = boundProjectKey
1475
+ ? `📍 当前 chat 绑定 Agent: ${boundProjectKey}`
1476
+ : `📍 当前 chat 使用全局默认引擎`;
1297
1477
  await bot.sendMessage(chatId, [
1298
- `🔧 引擎: ${cur} | Provider: ${activeProvider}`,
1478
+ `🔧 引擎: ${safeCur} | Provider: ${activeProvider}`,
1299
1479
  `🤖 会话模型: ${currentModel} | 后台轻量: ${distill}`,
1480
+ scopeLine,
1300
1481
  '',
1301
1482
  '用法: /engine claude 或 /engine codex',
1302
- '切换引擎将自动同步 distill model 和首选 provider',
1483
+ boundProjectKey
1484
+ ? '当前 chat 已绑定 Agent;切换时会同步更新该 Agent 的 engine/model'
1485
+ : '切换引擎将自动同步 distill model 和首选 provider',
1303
1486
  ].join('\n'));
1304
1487
  return { handled: true, config };
1305
1488
  }
@@ -1312,13 +1495,29 @@ function createAdminCommandHandler(deps) {
1312
1495
 
1313
1496
  setDefaultEngine(arg); // syncs distill model + providerMod.setEngine (no longer resets session model)
1314
1497
  const distill = getDistillModel();
1315
- const freshCfg = loadConfig();
1498
+ let freshCfg = loadConfig();
1499
+ if (boundProjectKey && freshCfg && freshCfg.projects && freshCfg.projects[boundProjectKey]) {
1500
+ const nextCfg = JSON.parse(JSON.stringify(freshCfg));
1501
+ nextCfg.projects[boundProjectKey].engine = arg;
1502
+ nextCfg.projects[boundProjectKey].model = resolveEngineModel(arg, nextCfg.daemon || {});
1503
+ writeConfigSafe(nextCfg);
1504
+ freshCfg = loadConfig();
1505
+ }
1316
1506
  const freshDaemon = freshCfg.daemon || {};
1317
- const syncedModel = resolveEngineModel(arg, freshDaemon);
1507
+ const syncedModel = resolveEngineModel(
1508
+ arg,
1509
+ freshDaemon,
1510
+ boundProjectKey && freshCfg.projects && freshCfg.projects[boundProjectKey]
1511
+ ? freshCfg.projects[boundProjectKey].model
1512
+ : ''
1513
+ );
1318
1514
 
1319
- // Auto-switch provider if the preferred one exists in providers.yaml
1515
+ // Auto-switch provider only for Claude-compatible routing.
1516
+ // Codex auth is handled by `codex login` / `OPENAI_API_KEY`, not providers.yaml.
1320
1517
  let providerNote = '';
1321
- if (providerMod && preferredProvider) {
1518
+ if (arg === 'codex') {
1519
+ providerNote = '\n🔌 Codex 认证: 使用 `codex login` 或 OPENAI_API_KEY(/provider 不参与 Codex 路由)';
1520
+ } else if (providerMod && preferredProvider) {
1322
1521
  try {
1323
1522
  providerMod.setActive(preferredProvider);
1324
1523
  providerNote = `\n🔌 Provider 已同步: ${preferredProvider}`;
@@ -1329,8 +1528,11 @@ function createAdminCommandHandler(deps) {
1329
1528
  }
1330
1529
  }
1331
1530
 
1332
- await bot.sendMessage(chatId, `✅ 引擎已切换: ${arg}\n🤖 会话模型: ${syncedModel}\n🧪 后台轻量模型: ${distill}${providerNote}`);
1333
- return { handled: true, config };
1531
+ const scopeNote = boundProjectKey
1532
+ ? `\n📍 已同步当前 Agent: ${boundProjectKey}`
1533
+ : '';
1534
+ await bot.sendMessage(chatId, `✅ 引擎已切换: ${arg}\n🤖 会话模型: ${syncedModel}\n🧪 后台轻量模型: ${distill}${scopeNote}${providerNote}`);
1535
+ return { handled: true, config: freshCfg };
1334
1536
  }
1335
1537
 
1336
1538
  // /distill-model [name] — show or update distill model