metame-cli 1.5.2 → 1.5.3

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.
@@ -35,11 +35,26 @@ function createSessionCommandHandler(deps) {
35
35
  return n === 'codex' ? 'codex' : getDefaultEngine();
36
36
  }
37
37
 
38
+ function getBoundCwd(chatId) {
39
+ try {
40
+ const cfg = loadConfig();
41
+ const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
42
+ const mappedKey = chatAgentMap[String(chatId)];
43
+ const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
44
+ return (proj && proj.cwd) ? normalizeCwd(proj.cwd) : null;
45
+ } catch { return null; }
46
+ }
47
+
38
48
  // Write per-engine session slot, preserving cwd and other engine slots.
39
49
  function attachEngineSession(state, chatId, engine, sessionId, cwd) {
40
- const existing = state.sessions[chatId] || {};
50
+ // For bound chats, resolve to virtual chatId so askClaude picks up the session
51
+ const cfg = loadConfig();
52
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
53
+ const boundKey = agentMap[String(chatId)];
54
+ const effectiveId = boundKey ? `_agent_${boundKey}` : String(chatId);
55
+ const existing = state.sessions[effectiveId] || {};
41
56
  const existingEngines = existing.engines || {};
42
- state.sessions[chatId] = {
57
+ state.sessions[effectiveId] = {
43
58
  ...existing,
44
59
  cwd: cwd || existing.cwd || HOME,
45
60
  engines: { ...existingEngines, [engine]: { id: sessionId, started: true } },
@@ -254,7 +269,7 @@ function createSessionCommandHandler(deps) {
254
269
  if (text === '/sessions') {
255
270
  const currentEngine = getDefaultEngine();
256
271
  const codexLimitTip = '⚠️ 当前为 Codex 会话:`/sessions` 列表暂仅展示 Claude 本地会话,Codex 会话暂不可见。';
257
- const allSessions = listRecentSessions(15);
272
+ const allSessions = listRecentSessions(15, getBoundCwd(chatId));
258
273
  if (allSessions.length === 0) {
259
274
  const base = 'No sessions found. Try /new first.';
260
275
  await bot.sendMessage(chatId, currentEngine === 'codex' ? `${base}\n\n${codexLimitTip}` : base);
@@ -365,14 +380,7 @@ function createSessionCommandHandler(deps) {
365
380
 
366
381
  // For bound chats, prefer sessions from the same project to avoid
367
382
  // the bound-chat guard (handleCommand) immediately overwriting with a new session.
368
- let boundCwd = null;
369
- try {
370
- const cfg = loadConfig();
371
- const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
372
- const mappedKey = chatAgentMap[String(chatId)];
373
- const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
374
- if (proj && proj.cwd) boundCwd = normalizeCwd(proj.cwd);
375
- } catch { /* ignore */ }
383
+ const boundCwd = getBoundCwd(chatId);
376
384
 
377
385
  let candidates = filtered;
378
386
  if (boundCwd) {
@@ -182,6 +182,11 @@ function createSessionStore(deps) {
182
182
  }
183
183
  }
184
184
  }
185
+ // Fallback: decode projectPath from directory name (e.g. -Users-yaron-AGI-AChat → /Users/yaron/AGI/AChat)
186
+ if (!projPathCache.has(proj) && proj.startsWith('-')) {
187
+ const decoded = proj.replace(/-/g, '/');
188
+ if (fs.existsSync(decoded)) projPathCache.set(proj, decoded);
189
+ }
185
190
  } catch { /* skip */ }
186
191
 
187
192
  try {
@@ -283,8 +288,7 @@ function createSessionStore(deps) {
283
288
  function listRecentSessions(limit, cwd) {
284
289
  let all = scanAllSessions();
285
290
  if (cwd) {
286
- const matched = all.filter(s => s.projectPath === cwd);
287
- if (matched.length > 0) all = matched;
291
+ all = all.filter(s => s.projectPath === cwd);
288
292
  }
289
293
  return all.slice(0, limit || 10);
290
294
  }
@@ -578,10 +582,24 @@ function createSessionStore(deps) {
578
582
  }
579
583
 
580
584
  // Claude backend: JSONL files under ~/.claude/projects/<hash>/
585
+ // Best approach: read cwd directly from session file content (not from dir name)
581
586
  function _isClaudeSessionValid(sessionId, normCwd) {
582
587
  try {
583
588
  const sessionFile = findSessionFile(sessionId);
584
589
  if (!sessionFile) return false;
590
+
591
+ // Try to read cwd from session JSONL file content (most reliable)
592
+ const content = fs.readFileSync(sessionFile, 'utf8');
593
+ const lines = content.split('\n').filter(l => l.trim());
594
+ for (const line of lines.slice(0, 20)) { // Check first 20 lines
595
+ try {
596
+ const entry = JSON.parse(line);
597
+ const fileCwd = entry.cwd || (entry.message && entry.message.cwd);
598
+ if (fileCwd && path.resolve(fileCwd) === normCwd) return true;
599
+ } catch { /* skip non-JSON lines */ }
600
+ }
601
+
602
+ // Fallback: check sessions-index.json if exists
585
603
  const projectDir = path.dirname(sessionFile);
586
604
  const indexFile = path.join(projectDir, 'sessions-index.json');
587
605
  if (fs.existsSync(indexFile)) {
@@ -592,15 +610,15 @@ function createSessionStore(deps) {
592
610
  const anyPath = (entries.find(e => e && e.projectPath) || {}).projectPath;
593
611
  if (anyPath) return path.resolve(anyPath) === normCwd;
594
612
  }
595
- // Weak fallback: Claude encodes cwd in dir name; only trust a positive match.
596
- // Unix: /home/user/project -home-user-project
597
- // Windows: D:\MetaMe D--MetaMe (replaces : and \ with -)
613
+
614
+ // Last resort fallback: dir name match (less reliable, skip for worktree paths)
615
+ // Skip this for paths containing .worktree to avoid edge cases
616
+ if (normCwd.includes('.worktree')) return true; // trust the session exists
598
617
  const actualDir = path.basename(projectDir).toLowerCase();
599
618
  const expectedDir = process.platform === 'win32'
600
619
  ? normCwd.replace(/[:\\\/_ ]/g, '-').toLowerCase()
601
- : ('-' + normCwd.replace(/^\//, '').replace(/[\/_ ]/g, '-')).toLowerCase();
602
- if (actualDir === expectedDir) return true;
603
- return false; // dir name mismatch — session belongs to a different project
620
+ : ('-' + normCwd.replace(/^\//, '').replace(/[\/_. ]/g, '-')).toLowerCase();
621
+ return actualDir === expectedDir;
604
622
  } catch {
605
623
  return true; // conservative: infra failure ≠ invalid session
606
624
  }
@@ -741,12 +741,12 @@ function createTaskScheduler(deps) {
741
741
  lastTickTime = tickNow;
742
742
 
743
743
  if (tickElapsed > WAKE_THRESHOLD) {
744
- log('FATAL', `[WAKE-DETECT] System resumed after ${Math.round(tickElapsed / 1000)}s sleep — restarting daemon to rebuild connections`);
744
+ log('WARN', `[WAKE-DETECT] System resumed after ${Math.round(tickElapsed / 1000)}s sleep — letting connections auto-reconnect`);
745
745
  const st = loadState();
746
746
  st.wake_restart = new Date().toISOString();
747
747
  st.wake_sleep_seconds = Math.round(tickElapsed / 1000);
748
748
  saveState(st);
749
- process.exit(1); // caffeinate will restart us with fresh connections
749
+ // Don't exit Feishu and Telegram have built-in auto-reconnect
750
750
  }
751
751
 
752
752
  // ① Physiological heartbeat (zero token, pure awareness)
package/scripts/daemon.js CHANGED
@@ -69,6 +69,12 @@ const CLAUDE_BIN = (() => {
69
69
  // Skill evolution module (hot path + cold path)
70
70
  let skillEvolution = null;
71
71
  try { skillEvolution = require('./skill-evolution'); } catch { /* graceful fallback */ }
72
+ const {
73
+ normalizeRemoteDispatchConfig,
74
+ encodePacket: encodeRemoteDispatchPacket,
75
+ decodePacket: decodeRemoteDispatchPacket,
76
+ verifyPacket: verifyRemoteDispatchPacket,
77
+ } = require('./daemon-remote-dispatch');
72
78
 
73
79
  // ---------------------------------------------------------
74
80
  // SKILL ROUTING (keyword → /skillname prefix, like metame-desktop)
@@ -145,7 +151,7 @@ const { createFileBrowser } = require('./daemon-file-browser');
145
151
  const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
146
152
  const { createNotifier } = require('./daemon-notify');
147
153
  const { createClaudeEngine } = require('./daemon-claude-engine');
148
- const { createEngineRuntimeFactory, detectDefaultEngine, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
154
+ const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
149
155
  const { createCommandRouter } = require('./daemon-command-router');
150
156
  const { createTaskScheduler } = require('./daemon-task-scheduler');
151
157
  const { createAgentTools } = require('./daemon-agent-tools');
@@ -515,6 +521,31 @@ let _handleCommand = null;
515
521
  let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
516
522
  function setDispatchHandler(fn) { _handleCommand = fn; }
517
523
 
524
+ function getRemoteDispatchConfig(config) {
525
+ return normalizeRemoteDispatchConfig(config || {});
526
+ }
527
+
528
+ async function sendRemoteDispatch(packet, config) {
529
+ const rd = getRemoteDispatchConfig(config);
530
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
531
+ if (!rd) return { success: false, error: 'feishu.remote_dispatch not configured' };
532
+ if (!liveBot || typeof liveBot.sendMessage !== 'function') return { success: false, error: 'feishu bot not connected' };
533
+ const ts = new Date().toISOString();
534
+ const id = `${rd.selfPeer}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
535
+ try {
536
+ const body = encodeRemoteDispatchPacket({
537
+ v: 1,
538
+ id,
539
+ ts,
540
+ ...packet,
541
+ }, rd.secret);
542
+ await liveBot.sendMessage(rd.chatId, body);
543
+ return { success: true, id };
544
+ } catch (e) {
545
+ return { success: false, error: e.message };
546
+ }
547
+ }
548
+
518
549
  /**
519
550
  * Create a null bot that captures Claude's output without sending to Feishu/Telegram.
520
551
  */
@@ -795,6 +826,76 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
795
826
  if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
796
827
  fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
797
828
 
829
+ // Auto-update now/shared.md and team shared files for cross-agent visibility
830
+ try {
831
+ const NOW_DIR = path.join(HOME, '.metame', 'memory', 'now');
832
+ const SHARED_FILE = path.join(NOW_DIR, 'shared.md');
833
+ const SHARED_DIR = path.join(HOME, '.metame', 'memory', 'shared');
834
+ if (!fs.existsSync(NOW_DIR)) fs.mkdirSync(NOW_DIR, { recursive: true });
835
+
836
+ const now = new Date();
837
+ const timeStr = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
838
+ const dateStr = now.toISOString().slice(0, 10);
839
+
840
+ // Get sender display name
841
+ const fromProj = config && config.projects ? config.projects[fullMsg.from] : null;
842
+ const fromName = fromProj ? (fromProj.name || fullMsg.from) : (fullMsg.from || 'unknown');
843
+ const fromIcon = fromProj ? (fromProj.icon || '🤖') : '🤖';
844
+
845
+ // Get target display name
846
+ const toProj = config && config.projects ? config.projects[targetProject] : null;
847
+ const toName = toProj ? (toProj.name || targetProject) : targetProject;
848
+ const toIcon = toProj ? (toProj.icon || '🤖') : '🤖';
849
+
850
+ const taskTitle = payload.title || '';
851
+ const taskPrompt = payload.prompt || '';
852
+
853
+ // Update shared.md
854
+ const content = `# 共享当前状态
855
+ **最后更新**: ${timeStr} **更新者**: ${fromName} (${fullMsg.from})
856
+
857
+ ## 当前任务
858
+ - **派发给**: ${toIcon} ${toName} (${targetProject})
859
+ - **任务**: ${taskTitle || taskPrompt.slice(0, 60)}
860
+ - **时间**: ${timeStr}
861
+
862
+ ## 任务链
863
+ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProject}
864
+ `;
865
+ fs.writeFileSync(SHARED_FILE, content, 'utf8');
866
+
867
+ // Update tasks.md if shared directory exists
868
+ const tasksFile = path.join(SHARED_DIR, 'tasks.md');
869
+ if (fs.existsSync(SHARED_DIR)) {
870
+ const taskLine = `- [${dateStr}] ${fromIcon} ${fromName} → ${toIcon} ${toName}: ${taskTitle || taskPrompt.slice(0, 40)}`;
871
+ let tasksContent = '';
872
+ if (fs.existsSync(tasksFile)) {
873
+ tasksContent = fs.readFileSync(tasksFile, 'utf8');
874
+ } else {
875
+ tasksContent = '# 任务看板\n\n## 🔄 进行中\n\n## ✅ 已完成\n\n## 📅 待开始\n';
876
+ }
877
+ // Insert task under "进行中" section
878
+ if (!tasksContent.includes(taskLine)) {
879
+ const lines = tasksContent.split('\n');
880
+ const newLines = [];
881
+ let inProgress = false;
882
+ for (const line of lines) {
883
+ newLines.push(line);
884
+ if (line.includes('## 🔄 进行中')) {
885
+ inProgress = true;
886
+ } else if (inProgress && line.startsWith('## ')) {
887
+ newLines.push(taskLine);
888
+ inProgress = false;
889
+ }
890
+ }
891
+ if (inProgress) newLines.push(taskLine);
892
+ fs.writeFileSync(tasksFile, newLines.join('\n'), 'utf8');
893
+ }
894
+ }
895
+ } catch (e) {
896
+ log('WARN', `Failed to update shared files: ${e.message}`);
897
+ }
898
+
798
899
  const rawPrompt = envelope
799
900
  ? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
800
901
  : (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
@@ -980,6 +1081,36 @@ function spawnSessionSummaries() {
980
1081
  /**
981
1082
  * Handle a single dispatch message (from socket or pending.jsonl fallback).
982
1083
  */
1084
+ /**
1085
+ * Find if both sender and target belong to the same team group.
1086
+ * Returns { parentKey, parentProject, senderMember, targetMember, groupChatId } or null.
1087
+ */
1088
+ function _findTeamBroadcastContext(fromKey, targetKey, config) {
1089
+ if (!config || !config.projects) return null;
1090
+ const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
1091
+ for (const [projKey, proj] of Object.entries(config.projects)) {
1092
+ if (!proj || !Array.isArray(proj.team) || proj.team.length === 0) continue;
1093
+ if (!proj.broadcast) continue; // broadcast switch must be on
1094
+ const senderMember = proj.team.find(m => m.key === fromKey);
1095
+ const targetMember = proj.team.find(m => m.key === targetKey);
1096
+ // Also check if sender/target is the parent project itself
1097
+ const senderIsParent = fromKey === projKey;
1098
+ const targetIsParent = targetKey === projKey;
1099
+ if ((senderMember || senderIsParent) && (targetMember || targetIsParent)) {
1100
+ // Find group chatId for this project
1101
+ const groupChatId = Object.entries(feishuMap).find(([, v]) => v === projKey)?.[0] || null;
1102
+ return {
1103
+ parentKey: projKey,
1104
+ parentProject: proj,
1105
+ senderMember: senderMember || { key: projKey, name: proj.name, icon: proj.icon || '🤖' },
1106
+ targetMember: targetMember || { key: projKey, name: proj.name, icon: proj.icon || '🤖' },
1107
+ groupChatId,
1108
+ };
1109
+ }
1110
+ }
1111
+ return null;
1112
+ }
1113
+
983
1114
  function handleDispatchItem(item, config) {
984
1115
  if (!item.target || !item.prompt) return;
985
1116
  if (!(config && config.projects && config.projects[item.target])) {
@@ -996,9 +1127,39 @@ function handleDispatchItem(item, config) {
996
1127
  return;
997
1128
  }
998
1129
  log('INFO', `Dispatch: ${item.from || '?'} → ${item.target}: ${item.prompt.slice(0, 60)}`);
1130
+
1131
+ // ── Team broadcast: intra-team dispatch → show in group chat ──
1132
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1133
+ const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, item.target, config) : null;
1134
+ if (teamCtx && teamCtx.groupChatId) {
1135
+ const { senderMember, targetMember, groupChatId, parentProject } = teamCtx;
1136
+ const sIcon = senderMember.icon || '🤖';
1137
+ const sName = senderMember.name || senderMember.key;
1138
+ const tIcon = targetMember.icon || '🤖';
1139
+ const tName = targetMember.name || targetMember.key;
1140
+ // Broadcast the handoff message to group as a card
1141
+ const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
1142
+ const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
1143
+ const cardColor = senderMember.color || 'blue';
1144
+ const sendFn = liveBot.sendCard
1145
+ ? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
1146
+ : () => liveBot.sendMarkdown(groupChatId, `**${cardTitle}**\n\n> ${cardBody}`);
1147
+ sendFn().catch(e => log('WARN', `Team broadcast failed: ${e.message}`));
1148
+ // Use streamForwardBot so target's reply also shows in group
1149
+ const streamOptions = { bot: liveBot, chatId: groupChatId };
1150
+ dispatchTask(item.target, {
1151
+ from: item.from || 'claude_session',
1152
+ type: 'task', priority: 'normal',
1153
+ payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
1154
+ callback: false,
1155
+ new_session: !!item.new_session,
1156
+ }, config, null, streamOptions);
1157
+ return;
1158
+ }
1159
+
1160
+ // ── Normal dispatch (non-team or broadcast off) ──
999
1161
  let pendingReplyFn = null;
1000
1162
  let streamOptions = null;
1001
- const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1002
1163
  if (liveBot) {
1003
1164
  const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
1004
1165
  const allowedFeishuIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
@@ -1016,7 +1177,21 @@ function handleDispatchItem(item, config) {
1016
1177
  const _userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
1017
1178
  let senderChatId = null;
1018
1179
  if (!_userSources.has(item.from)) {
1180
+ // Direct match: sender is a bound agent
1019
1181
  senderChatId = Object.entries(feishuMap).find(([, v]) => v === item.from)?.[0] || null;
1182
+ // Team member fallback: if sender is a team member (e.g., jarvis_c), find parent project's chatId
1183
+ if (!senderChatId) {
1184
+ const projects = config.projects || {};
1185
+ for (const [projKey, proj] of Object.entries(projects)) {
1186
+ if (proj.team && Array.isArray(proj.team)) {
1187
+ const member = proj.team.find(m => m.key === item.from);
1188
+ if (member && feishuMap[projKey]) {
1189
+ senderChatId = feishuMap[projKey];
1190
+ break;
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1020
1195
  }
1021
1196
  if (!senderChatId) {
1022
1197
  senderChatId = allowedFeishuIds.map(String).find(id => !agentChatIds.has(id)) || null;
@@ -1038,6 +1213,8 @@ function handleDispatchItem(item, config) {
1038
1213
  );
1039
1214
  });
1040
1215
  };
1216
+ // Also set streamOptions so target agent's streaming replies go to the sender's group
1217
+ streamOptions = { bot: liveBot, chatId: senderChatId };
1041
1218
  }
1042
1219
  }
1043
1220
  }
@@ -1050,6 +1227,90 @@ function handleDispatchItem(item, config) {
1050
1227
  }, config, pendingReplyFn, streamOptions);
1051
1228
  }
1052
1229
 
1230
+ async function handleRemoteDispatchMessage({ chatId, text, config }) {
1231
+ const rd = getRemoteDispatchConfig(config);
1232
+ if (!rd || String(chatId) !== rd.chatId) return false;
1233
+
1234
+ const packet = decodeRemoteDispatchPacket(text);
1235
+ if (!packet) return true;
1236
+ if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
1237
+ log('WARN', 'Remote dispatch ignored: invalid signature');
1238
+ return true;
1239
+ }
1240
+ if (packet.from_peer === rd.selfPeer) return true;
1241
+ if (packet.to_peer !== rd.selfPeer) return true;
1242
+
1243
+ if (packet.type === 'task') {
1244
+ const replyFn = async (output) => {
1245
+ const res = await sendRemoteDispatch({
1246
+ type: 'result',
1247
+ from_peer: rd.selfPeer,
1248
+ to_peer: packet.from_peer,
1249
+ target_project: packet.target_project,
1250
+ source_chat_id: packet.source_chat_id,
1251
+ source_sender_key: packet.source_sender_key || 'user',
1252
+ request_id: packet.id,
1253
+ result: String(output || '').slice(0, 4000),
1254
+ }, config);
1255
+ if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
1256
+ };
1257
+
1258
+ handleDispatchItem({
1259
+ target: packet.target_project,
1260
+ prompt: packet.prompt,
1261
+ from: packet.source_sender_key || `${packet.from_peer}:remote`,
1262
+ new_session: !!packet.new_session,
1263
+ _replyFn: replyFn,
1264
+ _suppressDefaultReplyRouting: true,
1265
+ }, config);
1266
+ return true;
1267
+ }
1268
+
1269
+ if (packet.type === 'result') {
1270
+ const targetChatId = String(packet.source_chat_id || '').trim();
1271
+ if (!targetChatId) {
1272
+ const inboxTarget = String(packet.source_sender_key || '').trim();
1273
+ if (!inboxTarget || inboxTarget === 'user' || inboxTarget === '_claude_session') {
1274
+ log('WARN', 'Remote dispatch result dropped: no source_chat_id/source_sender_key');
1275
+ return true;
1276
+ }
1277
+ try {
1278
+ const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', inboxTarget);
1279
+ fs.mkdirSync(inboxDir, { recursive: true });
1280
+ const tsStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15);
1281
+ const inboxFile = path.join(inboxDir, `${tsStr}_${packet.from_peer}_${packet.target_project || 'remote'}_result.md`);
1282
+ const body = [
1283
+ `FROM: ${packet.from_peer}:${packet.target_project || 'unknown'}`,
1284
+ `TO: ${inboxTarget}`,
1285
+ `TS: ${new Date().toISOString()}`,
1286
+ '',
1287
+ String(packet.result || '').trim() || '(empty result)',
1288
+ ].join('\n');
1289
+ fs.writeFileSync(inboxFile, body, 'utf8');
1290
+ } catch (e) {
1291
+ log('WARN', `Remote dispatch inbox write failed: ${e.message}`);
1292
+ }
1293
+ return true;
1294
+ }
1295
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1296
+ if (!liveBot) {
1297
+ log('WARN', 'Remote dispatch result dropped: no live bot');
1298
+ return true;
1299
+ }
1300
+ const header = `${packet.from_peer}:${packet.target_project || 'unknown'}`;
1301
+ const body = `${header}\n\n${String(packet.result || '').trim() || '(empty result)'}`;
1302
+ try {
1303
+ if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, body);
1304
+ else await liveBot.sendMessage(targetChatId, body);
1305
+ } catch (e) {
1306
+ log('WARN', `Remote dispatch result delivery failed: ${e.message}`);
1307
+ }
1308
+ return true;
1309
+ }
1310
+
1311
+ return true;
1312
+ }
1313
+
1053
1314
  /**
1054
1315
  * Start Unix Domain Socket server for low-latency dispatch.
1055
1316
  */
@@ -1528,6 +1789,7 @@ const { handleAdminCommand } = createAdminCommandHandler({
1528
1789
  skillEvolution,
1529
1790
  taskBoard,
1530
1791
  taskEnvelope,
1792
+ sendRemoteDispatch,
1531
1793
  getActiveProcesses: () => activeProcesses,
1532
1794
  getMessageQueue: () => messageQueue,
1533
1795
  loadState,
@@ -1765,6 +2027,8 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
1765
2027
  getSession,
1766
2028
  handleCommand,
1767
2029
  pendingActivations,
2030
+ activeProcesses,
2031
+ messageQueue,
1768
2032
  });
1769
2033
 
1770
2034
  const { killExistingDaemon, writePid, cleanPid } = createPidManager({
@@ -1900,7 +2164,9 @@ async function main() {
1900
2164
  'enable_nl_mac_fallback',
1901
2165
  ];
1902
2166
  // All known models across all engines (for legacy daemon.model validation only)
1903
- const BUILTIN_CLAUDE_MODELS = ENGINE_MODEL_CONFIG.claude.options;
2167
+ const BUILTIN_CLAUDE_MODELS = (ENGINE_MODEL_CONFIG.claude.options || []).map(option =>
2168
+ typeof option === 'string' ? option : option.value
2169
+ ).filter(Boolean);
1904
2170
  for (const key of Object.keys(config)) {
1905
2171
  if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
1906
2172
  }
@@ -1914,7 +2180,7 @@ async function main() {
1914
2180
  if (activeProv === 'anthropic' && _defaultEngine === 'claude') {
1915
2181
  log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known Claude model`);
1916
2182
  } else {
1917
- log('INFO', `Config: model "${config.daemon.model}" for engine "${_defaultEngine}" / provider "${activeProv}"`);
2183
+ log('INFO', `Config: legacy daemon.model="${config.daemon.model}" retained; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
1918
2184
  }
1919
2185
  }
1920
2186
  }
@@ -2162,5 +2428,5 @@ if (process.argv.includes('--run')) {
2162
2428
  });
2163
2429
  }
2164
2430
 
2165
- // Export for testing
2166
- module.exports = { executeTask, loadConfig, loadState, buildProfilePreamble, parseInterval };
2431
+ // Export for testing & cross-bot dispatch
2432
+ module.exports = { executeTask, loadConfig, loadState, buildProfilePreamble, parseInterval, handleRemoteDispatchMessage, sendRemoteDispatch };