metame-cli 1.5.3 → 1.5.5

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 (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
package/scripts/daemon.js CHANGED
@@ -47,7 +47,7 @@ const LOG_FILE = path.join(METAME_DIR, 'daemon.log');
47
47
  const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
48
48
  const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
49
49
  const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
50
- const { socketPath, needsSocketCleanup } = require('./platform');
50
+ const { sleepSync, socketPath, needsSocketCleanup } = require('./platform');
51
51
  const SOCK_PATH = socketPath(METAME_DIR);
52
52
 
53
53
  // Resolve claude binary path (daemon may not inherit user's full PATH)
@@ -69,11 +69,14 @@ 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
+ let userAcl = null;
73
+ try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
72
74
  const {
73
75
  normalizeRemoteDispatchConfig,
74
76
  encodePacket: encodeRemoteDispatchPacket,
75
77
  decodePacket: decodeRemoteDispatchPacket,
76
78
  verifyPacket: verifyRemoteDispatchPacket,
79
+ isDuplicate: isRemoteDispatchDuplicate,
77
80
  } = require('./daemon-remote-dispatch');
78
81
 
79
82
  // ---------------------------------------------------------
@@ -83,18 +86,15 @@ function isMacLocalOrchestratorIntent(prompt) {
83
86
  const text = String(prompt || '').trim();
84
87
  if (!text) return false;
85
88
 
86
- // Explicit macOS automation keywords.
87
- if (/\b(?:mac|macos|applescript|osascript|jxa|hammerspoon|aerospace|yabai|skhd|raycast|launchctl|keyboard maestro)\b/i.test(text)) {
88
- return true;
89
- }
90
- if (/(自动化|辅助功能|系统设置|隐私|权限|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|音量)/.test(text)) {
91
- return true;
92
- }
89
+ const hasAutomationVerb = /(?:自动化|脚本|控制|操作|执行|设置|调整|打开|关闭|启动|退出|切到|唤起|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|调(?:高|低|整)?音量|open|launch|quit|activate|lock\s*screen|sleep|mute|unmute|set\s+volume|run\s+(?:an?\s+)?script)/i.test(text);
90
+ const hasMacTool = /\b(?:mac|macos|applescript|osascript|jxa|hammerspoon|aerospace|yabai|skhd|raycast|launchctl|keyboard maestro|shortcuts)\b/i.test(text);
91
+ const hasMacTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|辅助功能|隐私|权限|屏幕录制|自动化|电脑|桌面|访达|System Events|LaunchAgent|快捷指令|锁屏|锁定屏幕|睡眠|休眠|静音|音量|mac)/i.test(text);
92
+
93
+ // Require an actual automation ask. Mentioning "macOS" or "权限" alone should not route.
94
+ if (hasMacTool && hasAutomationVerb) return true;
93
95
 
94
- // General verbs must be paired with explicit macOS targets to avoid over-routing.
95
- const hasAction = /(?:打开|关闭|启动|退出|切到|唤起|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|调(?:高|低|整)?音量|open|launch|quit|activate|lock\s*screen|sleep|mute|unmute)/i.test(text);
96
- const hasTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|电脑|System Events|mac)/i.test(text);
97
- return hasAction && hasTarget;
96
+ // Natural-language control only triggers when both the action and the macOS target are explicit.
97
+ return hasAutomationVerb && hasMacTarget;
98
98
  }
99
99
 
100
100
  const SKILL_ROUTES = [
@@ -147,11 +147,13 @@ const { createSessionCommandHandler } = require('./daemon-session-commands');
147
147
  const { createSessionStore } = require('./daemon-session-store');
148
148
  const { createCheckpointUtils } = require('./daemon-checkpoints');
149
149
  const { createBridgeStarter } = require('./daemon-bridges');
150
+ const { buildTeamRosterHint, buildEnrichedPrompt, resolveDispatchActor, updateDispatchContextFiles } = require('./team-dispatch');
150
151
  const { createFileBrowser } = require('./daemon-file-browser');
151
152
  const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
153
+ const { repairAgentLayer } = require('./agent-layer');
152
154
  const { createNotifier } = require('./daemon-notify');
153
155
  const { createClaudeEngine } = require('./daemon-claude-engine');
154
- const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
156
+ const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
155
157
  const { createCommandRouter } = require('./daemon-command-router');
156
158
  const { createTaskScheduler } = require('./daemon-task-scheduler');
157
159
  const { createAgentTools } = require('./daemon-agent-tools');
@@ -409,6 +411,24 @@ function saveState(state) {
409
411
  if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
410
412
  next.usage.updated_at = currentUsageUpdated;
411
413
  }
414
+
415
+ // Merge sessions: prevent concurrent agents from wiping each other's session data.
416
+ // When a stale state object is saved (e.g. after a long spawnClaudeStreaming await),
417
+ // preserve any sessions that were added/updated by other agents in the interim.
418
+ if (current.sessions && typeof current.sessions === 'object') {
419
+ if (!next.sessions || typeof next.sessions !== 'object') next.sessions = {};
420
+ for (const [key, curSession] of Object.entries(current.sessions)) {
421
+ if (!next.sessions[key]) {
422
+ // Session exists in cache but not in incoming state → preserve it
423
+ next.sessions[key] = curSession;
424
+ } else {
425
+ // Both have it → keep whichever has newer last_active
426
+ const curActive = Number(curSession && curSession.last_active) || 0;
427
+ const nextActive = Number(next.sessions[key] && next.sessions[key].last_active) || 0;
428
+ if (curActive > nextActive) next.sessions[key] = curSession;
429
+ }
430
+ }
431
+ }
412
432
  }
413
433
 
414
434
  _cachedState = next;
@@ -499,15 +519,6 @@ function recordTokens(state, tokens, meta = null) {
499
519
  }
500
520
 
501
521
 
502
- function getBudgetWarning(config, state) {
503
- const limit = (config.budget && config.budget.daily_limit) || 50000;
504
- const threshold = (config.budget && config.budget.warning_threshold) || 0.8;
505
- const ratio = state.budget.tokens_used / limit;
506
- if (ratio >= 1) return 'exceeded';
507
- if (ratio >= threshold) return 'warning';
508
- return 'ok';
509
- }
510
-
511
522
  const taskBoard = createTaskBoard({
512
523
  logger: (msg) => log('WARN', msg),
513
524
  });
@@ -519,12 +530,58 @@ const taskBoard = createTaskBoard({
519
530
  // Late-bound reference to handleCommand (defined later in file)
520
531
  let _handleCommand = null;
521
532
  let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
533
+ const _pendingRemoteDispatches = new Map();
522
534
  function setDispatchHandler(fn) { _handleCommand = fn; }
523
535
 
524
536
  function getRemoteDispatchConfig(config) {
525
537
  return normalizeRemoteDispatchConfig(config || {});
526
538
  }
527
539
 
540
+ function trackRemoteDispatch(packet) {
541
+ if (!packet || packet.type !== 'task') return;
542
+ const requestId = String(packet.id || '').trim();
543
+ const targetChatId = String(packet.source_chat_id || '').trim();
544
+ if (!requestId || !targetChatId) return;
545
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
546
+ const timeoutMs = 15000;
547
+ const existing = _pendingRemoteDispatches.get(requestId);
548
+ if (existing && existing.timer) clearTimeout(existing.timer);
549
+ const timer = setTimeout(async () => {
550
+ _pendingRemoteDispatches.delete(requestId);
551
+ const text = [
552
+ '⏱️ 远端 Dispatch 超时',
553
+ '',
554
+ `目标: ${packet.to_peer}:${packet.target_project || 'unknown'}`,
555
+ `请求: ${requestId}`,
556
+ `状态: 15s 内未收到回执`,
557
+ ].join('\n');
558
+ log('WARN', `Remote dispatch timeout id=${requestId} target=${packet.to_peer}:${packet.target_project || 'unknown'}`);
559
+ if (!liveBot) return;
560
+ try {
561
+ if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
562
+ else await liveBot.sendMessage(targetChatId, text);
563
+ } catch (e) {
564
+ log('WARN', `Remote dispatch timeout delivery failed: ${e.message}`);
565
+ }
566
+ }, timeoutMs);
567
+ _pendingRemoteDispatches.set(requestId, {
568
+ id: requestId,
569
+ targetChatId,
570
+ targetPeer: String(packet.to_peer || '').trim(),
571
+ targetProject: String(packet.target_project || '').trim(),
572
+ timer,
573
+ });
574
+ }
575
+
576
+ function resolveTrackedRemoteDispatch(requestId) {
577
+ const key = String(requestId || '').trim();
578
+ if (!key) return null;
579
+ const tracked = _pendingRemoteDispatches.get(key) || null;
580
+ if (tracked && tracked.timer) clearTimeout(tracked.timer);
581
+ if (tracked) _pendingRemoteDispatches.delete(key);
582
+ return tracked;
583
+ }
584
+
528
585
  async function sendRemoteDispatch(packet, config) {
529
586
  const rd = getRemoteDispatchConfig(config);
530
587
  const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
@@ -538,8 +595,13 @@ async function sendRemoteDispatch(packet, config) {
538
595
  id,
539
596
  ts,
540
597
  ...packet,
598
+ from_peer: rd.selfPeer,
541
599
  }, rd.secret);
542
600
  await liveBot.sendMessage(rd.chatId, body);
601
+ log('INFO', `Remote dispatch sent type=${packet.type} id=${id} to=${packet.to_peer}:${packet.target_project || 'unknown'} via=${rd.chatId}`);
602
+ if (packet.type === 'task') {
603
+ trackRemoteDispatch({ ...packet, id }, config);
604
+ }
543
605
  return { success: true, id };
544
606
  } catch (e) {
545
607
  return { success: false, error: e.message };
@@ -565,38 +627,91 @@ function createNullBot(onOutput) {
565
627
  };
566
628
  }
567
629
 
630
+ function stripLeadingPlanSection(text) {
631
+ const src = String(text || '');
632
+ if (!src.trim()) return '';
633
+ const normalized = src.replace(/\r\n/g, '\n');
634
+ const paragraphs = normalized.split(/\n\s*\n/);
635
+ if (paragraphs.length === 0) return normalized.trim();
636
+ const first = String(paragraphs[0] || '').trim();
637
+ if (!/^计划[::]/.test(first)) return normalized.trim();
638
+ const rest = paragraphs.slice(1).join('\n\n').trim();
639
+ if (rest) return rest;
640
+ const lines = normalized.split('\n');
641
+ const remaining = lines.slice(1).join('\n').trim();
642
+ return remaining || first.replace(/^计划[::]\s*/, '').trim();
643
+ }
644
+
568
645
  /**
569
646
  * Forward bot: routes all calls to a real bot with a fixed chatId.
570
647
  * Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
571
648
  */
572
- function createStreamForwardBot(realBot, chatId, onOutput = null) {
649
+ function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
573
650
  // Track edit-broken state independently so dispatch failures don't poison realBot's flag
574
651
  let _editBroken = false;
652
+ const ready = opts && opts.ready && typeof opts.ready.then === 'function'
653
+ ? opts.ready.catch(() => {})
654
+ : Promise.resolve();
655
+ async function waitUntilReady() {
656
+ await ready;
657
+ }
658
+ function normalizeOutput(payload) {
659
+ const text = typeof payload === 'object'
660
+ ? (payload.body || payload.title || JSON.stringify(payload))
661
+ : String(payload);
662
+ return opts.stripPlan !== false ? stripLeadingPlanSection(text) : text;
663
+ }
664
+ async function deliver(text, rawText = text) {
665
+ const displayText = normalizeOutput(text);
666
+ if (onOutput) onOutput(rawText);
667
+ if (opts.responseCard && realBot.sendCard) {
668
+ return realBot.sendCard(chatId, {
669
+ title: opts.responseCard.title,
670
+ body: displayText,
671
+ color: opts.responseCard.color || 'blue',
672
+ });
673
+ }
674
+ return realBot.sendMessage(chatId, displayText);
675
+ }
575
676
  return {
576
677
  sendMessage: async (_, text) => {
678
+ await waitUntilReady();
577
679
  log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
578
- if (onOutput) onOutput(text);
579
- return realBot.sendMessage(chatId, text);
680
+ return deliver(text, text);
580
681
  },
581
682
  sendMarkdown: async (_, text) => {
683
+ await waitUntilReady();
582
684
  log('INFO', `[StreamBot→${chatId.slice(-8)}] md: ${String(text).slice(0, 80)}`);
685
+ if (opts.responseCard && realBot.sendCard) {
686
+ const displayText = normalizeOutput(text);
687
+ if (onOutput) onOutput(text);
688
+ return realBot.sendCard(chatId, {
689
+ title: opts.responseCard.title,
690
+ body: displayText,
691
+ color: opts.responseCard.color || 'blue',
692
+ });
693
+ }
694
+ const displayText = normalizeOutput(text);
583
695
  if (onOutput) onOutput(text);
584
- return realBot.sendMarkdown(chatId, text);
696
+ return realBot.sendMarkdown(chatId, displayText);
585
697
  },
586
698
  sendCard: async (_, card) => {
699
+ await waitUntilReady();
587
700
  const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
588
701
  log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
589
702
  if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
590
703
  return realBot.sendCard(chatId, card);
591
704
  },
592
705
  sendRawCard: async (_, header, elements) => {
706
+ await waitUntilReady();
593
707
  log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
594
708
  if (onOutput) onOutput(header);
595
709
  return realBot.sendRawCard(chatId, header, elements);
596
710
  },
597
- sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
598
- sendTyping: async () => realBot.sendTyping(chatId),
711
+ sendButtons: async (_, text, buttons) => { await waitUntilReady(); return realBot.sendButtons(chatId, text, buttons); },
712
+ sendTyping: async () => { await waitUntilReady(); return realBot.sendTyping(chatId); },
599
713
  editMessage: async (_, msgId, text) => {
714
+ await waitUntilReady();
600
715
  if (_editBroken) return false;
601
716
  log('INFO', `[StreamBot→${chatId.slice(-8)}] edit ${String(msgId).slice(-8)}: ${String(text).slice(0, 60)}`);
602
717
  try {
@@ -609,8 +724,8 @@ function createStreamForwardBot(realBot, chatId, onOutput = null) {
609
724
  return false;
610
725
  }
611
726
  },
612
- deleteMessage: async (_, msgId) => realBot.deleteMessage(chatId, msgId),
613
- sendFile: async (_, filePath, caption) => realBot.sendFile(chatId, filePath, caption),
727
+ deleteMessage: async (_, msgId) => { await waitUntilReady(); return realBot.deleteMessage(chatId, msgId); },
728
+ sendFile: async (_, filePath, caption) => { await waitUntilReady(); return realBot.sendFile(chatId, filePath, caption); },
614
729
  downloadFile: async (...args) => realBot.downloadFile(...args),
615
730
  };
616
731
  }
@@ -780,6 +895,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
780
895
  const fullMsg = {
781
896
  id: `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
782
897
  from: message.from || 'unknown',
898
+ source_sender_id: String(message.source_sender_id || '').trim() || '',
783
899
  to: targetProject,
784
900
  type: message.type || 'task',
785
901
  priority: message.priority || 'normal',
@@ -792,6 +908,17 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
792
908
  created_at: new Date().toISOString(),
793
909
  };
794
910
 
911
+ // Inject team roster hint if target is a team member and hint not already present
912
+ if (!message.team_roster_injected && config && config.projects && fullMsg.payload.prompt) {
913
+ for (const [parentKey, parent] of Object.entries(config.projects)) {
914
+ if (Array.isArray(parent.team) && parent.team.some(m => m.key === targetProject)) {
915
+ const hint = buildTeamRosterHint(parentKey, targetProject, config.projects);
916
+ if (hint) fullMsg.payload.prompt = `${hint}\n\n---\n${fullMsg.payload.prompt}`;
917
+ break;
918
+ }
919
+ }
920
+ }
921
+
795
922
  if (envelope && taskBoard) {
796
923
  const nowIso = new Date().toISOString();
797
924
  taskBoard.upsertTask({
@@ -826,79 +953,23 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
826
953
  if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
827
954
  fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
828
955
 
829
- // Auto-update now/shared.md and team shared files for cross-agent visibility
956
+ // Auto-update scoped dispatch context files; only TeamTask writes shared state.
830
957
  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
- }
958
+ updateDispatchContextFiles({
959
+ fs,
960
+ path,
961
+ baseDir: METAME_DIR,
962
+ fullMsg,
963
+ targetProject,
964
+ config,
965
+ envelope,
966
+ logger: (msg) => log('WARN', msg),
967
+ });
895
968
  } catch (e) {
896
- log('WARN', `Failed to update shared files: ${e.message}`);
969
+ log('WARN', `Failed to update dispatch context files: ${e.message}`);
897
970
  }
898
971
 
899
- const rawPrompt = envelope
900
- ? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
901
- : (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
972
+ const rawPrompt = buildDispatchPrompt(targetProject, fullMsg, envelope);
902
973
 
903
974
  // Inject sender identity when dispatched by another agent (not directly from user)
904
975
  const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
@@ -924,10 +995,25 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
924
995
  const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
925
996
  const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
926
997
  log('INFO', `Dispatching ${fullMsg.type} to ${targetProject} via ${sessionMode}: ${rawPrompt.slice(0, 80)}`);
998
+ const streamReady = streamOptions?.bot && streamOptions?.chatId
999
+ ? (() => {
1000
+ if (typeof streamOptions.preDispatch === 'function') {
1001
+ return Promise.resolve()
1002
+ .then(() => streamOptions.preDispatch())
1003
+ .catch(e => log('WARN', `Dispatch prelude failed: ${e.message}`));
1004
+ }
1005
+ if (streamOptions.sendTaskCard === false) return Promise.resolve();
1006
+ const card = buildDispatchTaskCard(fullMsg, targetProject, config);
1007
+ return Promise.resolve()
1008
+ .then(() => sendDispatchTaskCard(streamOptions.bot, streamOptions.chatId, card))
1009
+ .catch(e => log('WARN', `Dispatch task card failed: ${e.message}`));
1010
+ })()
1011
+ : Promise.resolve();
927
1012
 
928
1013
  let _taskFinalized = false;
929
1014
  const outputHandler = (output) => {
930
1015
  const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
1016
+ const displayOut = envelope ? appendTeamTaskResumeHint(outStr, envelope.task_id, envelope.scope_id) : outStr;
931
1017
  log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
932
1018
  if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
933
1019
  const status = inferTaskStatusFromOutput(outStr);
@@ -946,7 +1032,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
946
1032
  _taskFinalized = true;
947
1033
  }
948
1034
  if (replyFn && outStr.trim().length > 2) {
949
- replyFn(outStr);
1035
+ replyFn(displayOut);
950
1036
  } else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
951
1037
  // Write result to sender's inbox before dispatching callback
952
1038
  try {
@@ -961,7 +1047,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
961
1047
  `TS: ${new Date().toISOString()}`,
962
1048
  `SUBJECT: ${subject}`,
963
1049
  '',
964
- outStr.trim().slice(0, 2000),
1050
+ displayOut.slice(0, 2000),
965
1051
  ].join('\n');
966
1052
  fs.writeFileSync(inboxFile, body, 'utf8');
967
1053
  } catch (e) {
@@ -969,12 +1055,13 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
969
1055
  }
970
1056
  dispatchTask(fullMsg.from, {
971
1057
  from: targetProject,
1058
+ source_sender_id: fullMsg.source_sender_id || '',
972
1059
  type: 'callback',
973
1060
  priority: 'normal',
974
1061
  payload: {
975
1062
  title: `任务完成: ${fullMsg.payload.title || fullMsg.id}`,
976
1063
  original_id: fullMsg.id,
977
- output: outStr.slice(0, 500),
1064
+ output: displayOut.slice(0, 500),
978
1065
  },
979
1066
  chain: [], // reset chain for callbacks
980
1067
  }, config);
@@ -983,11 +1070,14 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
983
1070
  // If streamOptions provided, use real bot so output appears in target's Feishu channel.
984
1071
  // Otherwise fall back to nullBot which captures output for replyFn.
985
1072
  const nullBot = streamOptions?.bot && streamOptions?.chatId
986
- ? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler)
1073
+ ? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler, {
1074
+ ready: streamReady,
1075
+ stripPlan: streamOptions.stripPlan !== false,
1076
+ responseCard: streamOptions.responseCard || null,
1077
+ })
987
1078
  : createNullBot(outputHandler);
988
- // Permission inheritance: if daemon runs with dangerously_skip_permissions, dispatched agents
989
- // inherit the same level they need Write access for implementation tasks.
990
- // Otherwise fall back to readOnly (safe default for untrusted daemon configs).
1079
+ // Trusted dispatches (user / bound agent / team member) keep write access.
1080
+ // Only unknown senders are downgraded to read-only.
991
1081
  // When forceNew=true, clear any cached session for this virtual chatId so
992
1082
  // attachOrCreateSession in handleCommand actually creates a fresh Claude session.
993
1083
  if (forceNew) {
@@ -997,7 +1087,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
997
1087
  saveState(st);
998
1088
  }
999
1089
  }
1000
- const dispatchReadOnly = !(config.daemon && config.daemon.dangerously_skip_permissions);
1090
+ const dispatchReadOnly = resolveDispatchReadOnly(message, config, targetProject);
1001
1091
  if (envelope && taskBoard) {
1002
1092
  taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
1003
1093
  taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
@@ -1010,7 +1100,12 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
1010
1100
  }
1011
1101
  });
1012
1102
 
1013
- return { success: true, id: fullMsg.id, task_id: envelope ? envelope.task_id : null };
1103
+ return {
1104
+ success: true,
1105
+ id: fullMsg.id,
1106
+ task_id: envelope ? envelope.task_id : null,
1107
+ scope_id: envelope ? envelope.scope_id : null,
1108
+ };
1014
1109
  }
1015
1110
 
1016
1111
  /**
@@ -1111,28 +1206,265 @@ function _findTeamBroadcastContext(fromKey, targetKey, config) {
1111
1206
  return null;
1112
1207
  }
1113
1208
 
1209
+ function resolveDispatchSenderChatId(item, config) {
1210
+ const requestedChatId = String(item && item.source_chat_id || '').trim();
1211
+ if (requestedChatId) return requestedChatId;
1212
+
1213
+ const feishuMap = (config && config.feishu && config.feishu.chat_agent_map) || {};
1214
+ const allowedFeishuIds = ((config && config.feishu && config.feishu.allowed_chat_ids) || []).map(String);
1215
+ const agentChatIds = new Set(Object.keys(feishuMap).map(String));
1216
+ const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
1217
+ const userSources = new Set(['', 'unknown', 'claude_session', '_claude_session', 'user']);
1218
+
1219
+ if (!userSources.has(senderKey)) {
1220
+ const directChatId = Object.entries(feishuMap).find(([, v]) => v === senderKey)?.[0] || null;
1221
+ if (directChatId) return String(directChatId);
1222
+
1223
+ const projects = (config && config.projects) || {};
1224
+ for (const [projKey, proj] of Object.entries(projects)) {
1225
+ if (!Array.isArray(proj && proj.team)) continue;
1226
+ const member = proj.team.find(m => m && m.key === senderKey);
1227
+ if (!member) continue;
1228
+ const groupChatId = Object.entries(feishuMap).find(([, v]) => v === projKey)?.[0] || null;
1229
+ if (groupChatId) return String(groupChatId);
1230
+ }
1231
+ }
1232
+
1233
+ return allowedFeishuIds.find(id => !agentChatIds.has(id)) || null;
1234
+ }
1235
+
1236
+ function writeDispatchReceiptInbox(item, receipt) {
1237
+ const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
1238
+ if (!senderKey || ['user', 'unknown', 'claude_session', '_claude_session'].includes(senderKey)) return;
1239
+ try {
1240
+ const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', senderKey);
1241
+ fs.mkdirSync(inboxDir, { recursive: true });
1242
+ const tsStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15);
1243
+ const targetKey = String(receipt && receipt.targetKey || item.target || 'unknown').trim() || 'unknown';
1244
+ const inboxFile = path.join(inboxDir, `${tsStr}_${targetKey}_dispatch_receipt.md`);
1245
+ const body = [
1246
+ `TYPE: dispatch_receipt`,
1247
+ `STATUS: ${receipt && receipt.status ? receipt.status : 'accepted'}`,
1248
+ `TARGET: ${targetKey}`,
1249
+ `DISPATCH_ID: ${receipt && receipt.dispatchId ? receipt.dispatchId : ''}`,
1250
+ `TS: ${new Date().toISOString()}`,
1251
+ '',
1252
+ String(receipt && receipt.text || '').trim() || '(empty receipt)',
1253
+ ].join('\n');
1254
+ fs.writeFileSync(inboxFile, body, 'utf8');
1255
+ } catch (e) {
1256
+ log('WARN', `Dispatch receipt inbox write failed: ${e.message}`);
1257
+ }
1258
+ }
1259
+
1260
+ function sendDispatchReceipt(item, config, receipt) {
1261
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1262
+ const senderChatId = resolveDispatchSenderChatId(item, config);
1263
+ const text = String(receipt && receipt.text || '').trim();
1264
+ if (!text) return;
1265
+
1266
+ if (liveBot && senderChatId) {
1267
+ const send = liveBot.sendMarkdown
1268
+ ? liveBot.sendMarkdown(senderChatId, text)
1269
+ : liveBot.sendMessage(senderChatId, text);
1270
+ send.catch((e) => {
1271
+ log('WARN', `Dispatch receipt delivery failed: ${e.message}`);
1272
+ writeDispatchReceiptInbox(item, receipt);
1273
+ });
1274
+ return;
1275
+ }
1276
+
1277
+ writeDispatchReceiptInbox(item, receipt);
1278
+ }
1279
+
1280
+ function buildTeamTaskResumeHint(taskId, scopeId) {
1281
+ const safeTaskId = String(taskId || '').trim();
1282
+ if (!safeTaskId) return '';
1283
+ const safeScopeId = String(scopeId || '').trim();
1284
+ const lines = [
1285
+ '',
1286
+ `TeamTask: ${safeTaskId}`,
1287
+ ];
1288
+ if (safeScopeId && safeScopeId !== safeTaskId) lines.push(`Scope: ${safeScopeId}`);
1289
+ lines.push(`如需复工,请使用: /TeamTask resume ${safeTaskId}`);
1290
+ return lines.join('\n');
1291
+ }
1292
+
1293
+ function appendTeamTaskResumeHint(text, taskId, scopeId) {
1294
+ const base = String(text || '').trim();
1295
+ const hint = buildTeamTaskResumeHint(taskId, scopeId);
1296
+ if (!hint) return base;
1297
+ return `${base}${hint}`;
1298
+ }
1299
+
1300
+ function buildDispatchPrompt(targetProject, fullMsg, envelope, metameDir = METAME_DIR) {
1301
+ const promptBody = buildEnrichedPrompt(
1302
+ targetProject,
1303
+ fullMsg && fullMsg.payload ? (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided') : 'No prompt provided',
1304
+ metameDir,
1305
+ { includeShared: !!(envelope && envelope.task_kind === 'team') }
1306
+ );
1307
+ return envelope
1308
+ ? buildPromptFromTaskEnvelope(envelope, promptBody)
1309
+ : promptBody;
1310
+ }
1311
+
1312
+ function resolveDispatchTarget(targetKey, config) {
1313
+ const rawKey = String(targetKey || '').trim();
1314
+ const projects = (config && config.projects) || {};
1315
+ if (!rawKey) return null;
1316
+ if (projects[rawKey]) {
1317
+ const proj = projects[rawKey];
1318
+ return {
1319
+ key: rawKey,
1320
+ name: proj.name || rawKey,
1321
+ icon: proj.icon || '🤖',
1322
+ color: proj.color || 'blue',
1323
+ parentKey: rawKey,
1324
+ parentProject: proj,
1325
+ member: null,
1326
+ isTeamMember: false,
1327
+ };
1328
+ }
1329
+ for (const [parentKey, parent] of Object.entries(projects)) {
1330
+ if (!Array.isArray(parent && parent.team)) continue;
1331
+ const member = parent.team.find(m => m && m.key === rawKey);
1332
+ if (!member) continue;
1333
+ return {
1334
+ key: rawKey,
1335
+ name: member.name || rawKey,
1336
+ icon: member.icon || parent.icon || '🤖',
1337
+ color: member.color || parent.color || 'blue',
1338
+ parentKey,
1339
+ parentProject: parent,
1340
+ member,
1341
+ isTeamMember: true,
1342
+ };
1343
+ }
1344
+ return null;
1345
+ }
1346
+
1347
+ function buildDispatchResponseCard(targetKey, config) {
1348
+ const target = resolveDispatchTarget(targetKey, config);
1349
+ if (!target) return null;
1350
+ return {
1351
+ title: `${target.icon} ${target.name}`,
1352
+ color: target.color || 'blue',
1353
+ };
1354
+ }
1355
+
1356
+ function buildDispatchTaskCard(fullMsg, targetProject, config) {
1357
+ const projects = (config && config.projects) || {};
1358
+ const actor = resolveDispatchActor(
1359
+ (fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from),
1360
+ projects
1361
+ );
1362
+ const target = resolveDispatchTarget(targetProject, config) || {
1363
+ icon: '🤖',
1364
+ name: targetProject,
1365
+ color: 'blue',
1366
+ };
1367
+ const prompt = String(fullMsg && fullMsg.payload && (fullMsg.payload.prompt || fullMsg.payload.title) || '').trim();
1368
+ const preview = prompt ? `${prompt.slice(0, 300)}${prompt.length > 300 ? '…' : ''}` : '(empty)';
1369
+ const lines = [
1370
+ `发起: ${actor.icon} ${actor.name}`,
1371
+ `目标: ${target.icon} ${target.name}`,
1372
+ `编号: ${fullMsg.id}`,
1373
+ ];
1374
+ if (fullMsg.task_id) lines.push(`TeamTask: ${fullMsg.task_id}`);
1375
+ if (fullMsg.scope_id && fullMsg.scope_id !== fullMsg.task_id) lines.push(`Scope: ${fullMsg.scope_id}`);
1376
+ lines.push('', preview);
1377
+ return {
1378
+ title: '📬 新任务',
1379
+ body: lines.join('\n'),
1380
+ color: target.color || 'blue',
1381
+ markdown: `## 📬 新任务\n\n${lines.join('\n')}\n\n---\n${preview}`,
1382
+ text: `📬 新任务\n\n${lines.join('\n')}\n\n${preview}`,
1383
+ };
1384
+ }
1385
+
1386
+ function resolveDispatchReadOnly(message, config, targetProject) {
1387
+ if (message && typeof message.readOnly === 'boolean') return message.readOnly;
1388
+ const senderId = String((message && message.source_sender_id) || '').trim();
1389
+ if (senderId && userAcl && typeof userAcl.resolveUserCtx === 'function') {
1390
+ try {
1391
+ const userCtx = userAcl.resolveUserCtx(senderId, config || {});
1392
+ return !!userCtx.readOnly;
1393
+ } catch { /* fall through to safe default */ }
1394
+ }
1395
+ void targetProject;
1396
+ return true;
1397
+ }
1398
+
1399
+ async function sendDispatchTaskCard(bot, chatId, card) {
1400
+ if (!bot || !chatId || !card) return null;
1401
+ if (bot.sendCard) return bot.sendCard(chatId, { title: card.title, body: card.body, color: card.color || 'blue' });
1402
+ if (bot.sendMarkdown) return bot.sendMarkdown(chatId, card.markdown);
1403
+ return bot.sendMessage(chatId, card.text);
1404
+ }
1405
+
1406
+ function buildDispatchReceipt(item, config, result, opts = {}) {
1407
+ const targetKey = String(opts.targetKey || item.target || '').trim() || 'unknown';
1408
+ const target = resolveDispatchTarget(targetKey, config) || {
1409
+ icon: '🤖',
1410
+ name: targetKey,
1411
+ };
1412
+ const actor = resolveDispatchActor(
1413
+ String(item && (item.source_sender_key || item.from) || 'user').trim() || 'user',
1414
+ (config && config.projects) || {}
1415
+ );
1416
+ const prompt = String(item && item.prompt || '').trim();
1417
+ const preview = prompt ? `${prompt.slice(0, 120)}${prompt.length > 120 ? '...' : ''}` : '(empty)';
1418
+ const isFailed = !result || !result.success;
1419
+ const title = isFailed ? '❌ Dispatch 回执' : '📮 Dispatch 回执';
1420
+ const statusLine = isFailed
1421
+ ? `状态: 入队失败 (${String(result && result.error || 'unknown_error').slice(0, 120)})`
1422
+ : '状态: 目标端已接收并入队';
1423
+ const lines = [
1424
+ title,
1425
+ '',
1426
+ statusLine,
1427
+ `发起: ${actor.icon} ${actor.name}`,
1428
+ `目标: ${target.icon} ${target.name}`,
1429
+ ];
1430
+ if (result && result.id) lines.push(`编号: ${result.id}`);
1431
+ lines.push(`摘要: ${preview}`);
1432
+ if (result && result.task_id) lines.push(buildTeamTaskResumeHint(result.task_id, result.scope_id));
1433
+ return {
1434
+ status: isFailed ? 'failed' : 'accepted',
1435
+ dispatchId: result && result.id ? result.id : '',
1436
+ targetKey,
1437
+ text: lines.join('\n'),
1438
+ };
1439
+ }
1440
+
1114
1441
  function handleDispatchItem(item, config) {
1115
1442
  if (!item.target || !item.prompt) return;
1116
- if (!(config && config.projects && config.projects[item.target])) {
1443
+ const resolvedTarget = resolveDispatchTarget(item.target, config);
1444
+ if (!resolvedTarget) {
1117
1445
  log('WARN', `dispatch: unknown target "${item.target}"`);
1118
- return;
1446
+ sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'unknown_target' }));
1447
+ return { success: false, error: 'unknown_target' };
1119
1448
  }
1449
+ const targetKey = resolvedTarget.key;
1120
1450
  // 安全护栏:禁止 agent 主动 dispatch 到 personal(防止 LLM 幻觉乱发消息给小美)
1121
1451
  // personal 只允许用户本人触发,或来源为 user/unknown 的系统任务
1122
1452
  const _agentSources = new Set(Object.keys((config.projects) || {}));
1123
1453
  const isFromAgent = _agentSources.has(item.from) || item.from === '_claude_session';
1124
- const targetProject = config.projects?.[item.target] || {};
1454
+ const targetProject = config.projects?.[targetKey] || {};
1125
1455
  if (isFromAgent && targetProject.guard === 'user-only') {
1126
- log('WARN', `dispatch: blocked agent "${item.from}" → "${item.target}" (user-only guard)`);
1127
- return;
1456
+ log('WARN', `dispatch: blocked agent "${item.from}" → "${targetKey}" (user-only guard)`);
1457
+ sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'target_guard_user_only' }));
1458
+ return { success: false, error: 'target_guard_user_only' };
1128
1459
  }
1129
- log('INFO', `Dispatch: ${item.from || '?'} → ${item.target}: ${item.prompt.slice(0, 60)}`);
1460
+ log('INFO', `Dispatch: ${item.from || '?'} → ${targetKey}: ${item.prompt.slice(0, 60)}`);
1130
1461
 
1131
1462
  // ── Team broadcast: intra-team dispatch → show in group chat ──
1132
1463
  const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1133
- const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, item.target, config) : null;
1464
+ const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, targetKey, config) : null;
1465
+ const responseCard = buildDispatchResponseCard(targetKey, config);
1134
1466
  if (teamCtx && teamCtx.groupChatId) {
1135
- const { senderMember, targetMember, groupChatId, parentProject } = teamCtx;
1467
+ const { senderMember, targetMember, groupChatId } = teamCtx;
1136
1468
  const sIcon = senderMember.icon || '🤖';
1137
1469
  const sName = senderMember.name || senderMember.key;
1138
1470
  const tIcon = targetMember.icon || '🤖';
@@ -1141,71 +1473,48 @@ function handleDispatchItem(item, config) {
1141
1473
  const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
1142
1474
  const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
1143
1475
  const cardColor = senderMember.color || 'blue';
1144
- const sendFn = liveBot.sendCard
1476
+ const sendTaskNotice = liveBot.sendCard
1145
1477
  ? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
1146
1478
  : () => 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, {
1479
+ // Use streamForwardBot so target's reply also shows in group.
1480
+ // Gate the worker output behind the task notice so the group always sees the task card first.
1481
+ const streamOptions = {
1482
+ bot: liveBot,
1483
+ chatId: groupChatId,
1484
+ preDispatch: () => sendTaskNotice().catch(e => log('WARN', `Team broadcast failed: ${e.message}`)),
1485
+ sendTaskCard: false,
1486
+ stripPlan: true,
1487
+ responseCard,
1488
+ };
1489
+ const result = dispatchTask(targetKey, {
1151
1490
  from: item.from || 'claude_session',
1491
+ source_sender_id: item.source_sender_id || '',
1152
1492
  type: 'task', priority: 'normal',
1153
1493
  payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
1154
1494
  callback: false,
1155
1495
  new_session: !!item.new_session,
1496
+ source_chat_id: item.source_chat_id || '',
1497
+ source_sender_key: item.source_sender_key || item.from || '',
1498
+ source_sender_id: item.source_sender_id || '',
1156
1499
  }, config, null, streamOptions);
1157
- return;
1500
+ sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
1501
+ return result;
1158
1502
  }
1159
1503
 
1160
1504
  // ── Normal dispatch (non-team or broadcast off) ──
1161
- let pendingReplyFn = null;
1505
+ let pendingReplyFn = typeof item._replyFn === 'function' ? item._replyFn : null;
1162
1506
  let streamOptions = null;
1163
1507
  if (liveBot) {
1164
1508
  const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
1165
- const allowedFeishuIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
1166
- const agentChatIds = new Set(Object.keys(feishuMap));
1167
- const targetChatId = Object.entries(feishuMap).find(([, v]) => v === item.target)?.[0] || null;
1509
+ const targetChatId = Object.entries(feishuMap).find(([, v]) => v === targetKey)?.[0] || null;
1168
1510
  if (targetChatId) {
1169
- streamOptions = { bot: liveBot, chatId: targetChatId };
1170
- const ackText = `📬 **新任务**\n\n> ${item.prompt.slice(0, 120)}${item.prompt.length > 120 ? '...' : ''}`;
1171
- liveBot.sendMarkdown(targetChatId, ackText).catch(() =>
1172
- liveBot.sendMessage(targetChatId, ackText.replace(/\*\*/g, '')).catch(e =>
1173
- log('WARN', `Dispatch ack failed: ${e.message}`)
1174
- )
1175
- );
1176
- } else {
1177
- const _userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
1178
- let senderChatId = null;
1179
- if (!_userSources.has(item.from)) {
1180
- // Direct match: sender is a bound agent
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
- }
1195
- }
1196
- if (!senderChatId) {
1197
- senderChatId = allowedFeishuIds.map(String).find(id => !agentChatIds.has(id)) || null;
1198
- }
1511
+ streamOptions = { bot: liveBot, chatId: targetChatId, stripPlan: true, responseCard };
1512
+ } else if (!item._suppressDefaultReplyRouting) {
1513
+ const senderChatId = resolveDispatchSenderChatId(item, config);
1199
1514
  if (senderChatId) {
1200
- const targetProj = (config.projects || {})[item.target] || {};
1201
- const ackText = `📬 已接收,转发给 ${targetProj.icon || '🤖'} **${targetProj.name || item.target}**...\n\n> ${item.prompt.slice(0, 100)}${item.prompt.length > 100 ? '...' : ''}`;
1202
- liveBot.sendMarkdown(senderChatId, ackText).catch(() =>
1203
- liveBot.sendMessage(senderChatId, ackText.replace(/\*\*/g, '')).catch(e =>
1204
- log('WARN', `Dispatch ack to sender failed: ${e.message}`)
1205
- )
1206
- );
1515
+ const targetProj = resolveDispatchTarget(targetKey, config) || {};
1207
1516
  pendingReplyFn = (output) => {
1208
- const text = `${targetProj.icon || '📬'} **${targetProj.name || item.target}** 回复:\n\n${output.slice(0, 2000)}`;
1517
+ const text = `${targetProj.icon || '📬'} **${targetProj.name || targetKey}** 回复:\n\n${output.slice(0, 2000)}`;
1209
1518
  liveBot.sendMarkdown(senderChatId, text).catch(e => {
1210
1519
  log('WARN', `Dispatch reply (markdown) failed: ${e.message}`);
1211
1520
  liveBot.sendMessage(senderChatId, text.replace(/\*\*/g, '')).catch(e2 =>
@@ -1214,31 +1523,52 @@ function handleDispatchItem(item, config) {
1214
1523
  });
1215
1524
  };
1216
1525
  // Also set streamOptions so target agent's streaming replies go to the sender's group
1217
- streamOptions = { bot: liveBot, chatId: senderChatId };
1526
+ streamOptions = { bot: liveBot, chatId: senderChatId, stripPlan: true, responseCard };
1218
1527
  }
1219
1528
  }
1220
1529
  }
1221
- dispatchTask(item.target, {
1530
+ const result = dispatchTask(targetKey, {
1222
1531
  from: item.from || 'claude_session',
1532
+ source_sender_id: item.source_sender_id || '',
1223
1533
  type: 'task', priority: 'normal',
1224
1534
  payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
1225
1535
  callback: false,
1226
1536
  new_session: !!item.new_session,
1537
+ source_chat_id: item.source_chat_id || '',
1538
+ source_sender_key: item.source_sender_key || item.from || '',
1539
+ source_sender_id: item.source_sender_id || '',
1227
1540
  }, config, pendingReplyFn, streamOptions);
1541
+ sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
1542
+ return result;
1228
1543
  }
1229
1544
 
1230
1545
  async function handleRemoteDispatchMessage({ chatId, text, config }) {
1231
1546
  const rd = getRemoteDispatchConfig(config);
1232
1547
  if (!rd || String(chatId) !== rd.chatId) return false;
1548
+ log('INFO', `Remote dispatch intercept chat=${chatId} preview=${String(text || '').slice(0, 48).replace(/\s+/g, ' ')}`);
1233
1549
 
1234
1550
  const packet = decodeRemoteDispatchPacket(text);
1235
- if (!packet) return true;
1551
+ if (!packet) {
1552
+ log('INFO', 'Remote dispatch decode miss');
1553
+ return true;
1554
+ }
1236
1555
  if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
1237
1556
  log('WARN', 'Remote dispatch ignored: invalid signature');
1238
1557
  return true;
1239
1558
  }
1240
- if (packet.from_peer === rd.selfPeer) return true;
1241
- if (packet.to_peer !== rd.selfPeer) return true;
1559
+ if (packet.from_peer === rd.selfPeer) {
1560
+ log('INFO', `Remote dispatch ignored: self echo id=${packet.id || ''}`);
1561
+ return true;
1562
+ }
1563
+ if (packet.to_peer !== rd.selfPeer) {
1564
+ log('INFO', `Remote dispatch ignored: peer mismatch id=${packet.id || ''} to=${packet.to_peer || ''} self=${rd.selfPeer}`);
1565
+ return true;
1566
+ }
1567
+ if (isRemoteDispatchDuplicate(packet.id)) {
1568
+ log('DEBUG', `Remote dispatch ignored: duplicate id=${packet.id}`);
1569
+ return true;
1570
+ }
1571
+ log('INFO', `Remote dispatch received type=${packet.type} id=${packet.id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
1242
1572
 
1243
1573
  if (packet.type === 'task') {
1244
1574
  const replyFn = async (output) => {
@@ -1249,24 +1579,89 @@ async function handleRemoteDispatchMessage({ chatId, text, config }) {
1249
1579
  target_project: packet.target_project,
1250
1580
  source_chat_id: packet.source_chat_id,
1251
1581
  source_sender_key: packet.source_sender_key || 'user',
1582
+ source_sender_id: packet.source_sender_id || '',
1252
1583
  request_id: packet.id,
1253
1584
  result: String(output || '').slice(0, 4000),
1254
1585
  }, config);
1255
1586
  if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
1256
1587
  };
1257
1588
 
1258
- handleDispatchItem({
1589
+ const dispatchRes = handleDispatchItem({
1259
1590
  target: packet.target_project,
1260
1591
  prompt: packet.prompt,
1261
1592
  from: packet.source_sender_key || `${packet.from_peer}:remote`,
1262
1593
  new_session: !!packet.new_session,
1594
+ source_chat_id: packet.source_chat_id || '',
1595
+ source_sender_key: packet.source_sender_key || '',
1596
+ source_sender_id: packet.source_sender_id || '',
1263
1597
  _replyFn: replyFn,
1264
1598
  _suppressDefaultReplyRouting: true,
1265
1599
  }, config);
1600
+ const ackRes = await sendRemoteDispatch({
1601
+ type: 'ack',
1602
+ to_peer: packet.from_peer,
1603
+ target_project: packet.target_project,
1604
+ source_chat_id: packet.source_chat_id,
1605
+ source_sender_key: packet.source_sender_key || 'user',
1606
+ source_sender_id: packet.source_sender_id || '',
1607
+ request_id: packet.id,
1608
+ dispatch_id: dispatchRes && dispatchRes.id ? dispatchRes.id : '',
1609
+ task_id: dispatchRes && dispatchRes.task_id ? dispatchRes.task_id : '',
1610
+ scope_id: dispatchRes && dispatchRes.scope_id ? dispatchRes.scope_id : '',
1611
+ status: dispatchRes && dispatchRes.success ? 'accepted' : 'failed',
1612
+ error: dispatchRes && dispatchRes.success ? '' : String(dispatchRes && dispatchRes.error || 'dispatch_failed'),
1613
+ }, config);
1614
+ if (!ackRes.success) log('WARN', `Remote dispatch ack send failed: ${ackRes.error}`);
1615
+ return true;
1616
+ }
1617
+
1618
+ if (packet.type === 'ack') {
1619
+ resolveTrackedRemoteDispatch(packet.request_id);
1620
+ log('INFO', `Remote dispatch ack id=${packet.request_id || ''} status=${packet.status} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
1621
+ const text = String(packet.status) === 'accepted'
1622
+ ? [
1623
+ '📮 远端 Dispatch 回执',
1624
+ '',
1625
+ `状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 已接收并入队`,
1626
+ packet.dispatch_id ? `编号: ${packet.dispatch_id}` : '',
1627
+ packet.task_id ? buildTeamTaskResumeHint(packet.task_id, packet.scope_id) : '',
1628
+ ].filter(Boolean).join('\n')
1629
+ : [
1630
+ '❌ 远端 Dispatch 回执',
1631
+ '',
1632
+ `状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 入队失败`,
1633
+ packet.error ? `错误: ${String(packet.error).slice(0, 200)}` : '',
1634
+ ].filter(Boolean).join('\n');
1635
+
1636
+ const targetChatId = String(packet.source_chat_id || '').trim();
1637
+ if (targetChatId) {
1638
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1639
+ if (!liveBot) {
1640
+ writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
1641
+ return true;
1642
+ }
1643
+ try {
1644
+ if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
1645
+ else await liveBot.sendMessage(targetChatId, text);
1646
+ } catch (e) {
1647
+ log('WARN', `Remote dispatch ack delivery failed: ${e.message}`);
1648
+ writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
1649
+ }
1650
+ return true;
1651
+ }
1652
+
1653
+ writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, {
1654
+ status: packet.status,
1655
+ targetKey: packet.target_project,
1656
+ dispatchId: packet.dispatch_id,
1657
+ text,
1658
+ });
1266
1659
  return true;
1267
1660
  }
1268
1661
 
1269
1662
  if (packet.type === 'result') {
1663
+ resolveTrackedRemoteDispatch(packet.request_id);
1664
+ log('INFO', `Remote dispatch result id=${packet.request_id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
1270
1665
  const targetChatId = String(packet.source_chat_id || '').trim();
1271
1666
  if (!targetChatId) {
1272
1667
  const inboxTarget = String(packet.source_sender_key || '').trim();
@@ -1324,8 +1719,8 @@ function startDispatchSocket(getConfig) {
1324
1719
  try {
1325
1720
  const item = JSON.parse(buf);
1326
1721
  const liveCfg = typeof getConfig === 'function' ? getConfig() : getConfig;
1327
- handleDispatchItem(item, liveCfg || {});
1328
- conn.write(JSON.stringify({ ok: true }) + '\n');
1722
+ const result = handleDispatchItem(item, liveCfg || {});
1723
+ conn.write(JSON.stringify({ ok: !!(result && result.success), id: result && result.id ? result.id : null, error: result && result.error ? result.error : null }) + '\n');
1329
1724
  } catch (e) {
1330
1725
  try { conn.write(JSON.stringify({ ok: false, error: e.message }) + '\n'); } catch { /* ignore */ }
1331
1726
  }
@@ -1369,6 +1764,41 @@ function physiologicalHeartbeat(config) {
1369
1764
  log('WARN', `Pending dispatch drain failed: ${e.message}`);
1370
1765
  }
1371
1766
 
1767
+ // 2b. Drain remote-pending.jsonl — remote dispatch packets written by dispatch_to CLI
1768
+ const REMOTE_PENDING = path.join(DISPATCH_DIR, 'remote-pending.jsonl');
1769
+ const REMOTE_PENDING_TMP = REMOTE_PENDING + '.processing';
1770
+ try {
1771
+ if (fs.existsSync(REMOTE_PENDING)) {
1772
+ fs.renameSync(REMOTE_PENDING, REMOTE_PENDING_TMP);
1773
+ const content = fs.readFileSync(REMOTE_PENDING_TMP, 'utf8').trim();
1774
+ fs.unlinkSync(REMOTE_PENDING_TMP);
1775
+ if (content) {
1776
+ const items = content.split('\n').filter(Boolean)
1777
+ .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
1778
+ const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
1779
+ for (const item of items) {
1780
+ if (item.relay_chat_id && item.body && liveBot && typeof liveBot.sendMessage === 'function') {
1781
+ const packet = decodeRemoteDispatchPacket(item.body);
1782
+ liveBot.sendMessage(item.relay_chat_id, item.body)
1783
+ .then(() => {
1784
+ if (packet) {
1785
+ log('INFO', `Remote dispatch queue sent type=${packet.type} id=${packet.id || ''} to=${packet.to_peer}:${packet.target_project || 'unknown'} via=${item.relay_chat_id}`);
1786
+ if (packet.type === 'task') trackRemoteDispatch(packet, config);
1787
+ } else {
1788
+ log('INFO', `Remote dispatch queue sent raw via=${item.relay_chat_id}`);
1789
+ }
1790
+ })
1791
+ .catch(e2 =>
1792
+ log('WARN', `Remote dispatch relay send failed: ${e2.message}`)
1793
+ );
1794
+ }
1795
+ }
1796
+ }
1797
+ }
1798
+ } catch (e) {
1799
+ log('WARN', `Remote pending dispatch drain failed: ${e.message}`);
1800
+ }
1801
+
1372
1802
  // 2. Rotate dispatch-log if > 512KB (keep 7 days)
1373
1803
  try {
1374
1804
  if (fs.existsSync(DISPATCH_LOG)) {
@@ -1391,7 +1821,6 @@ function physiologicalHeartbeat(config) {
1391
1821
  const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
1392
1822
  const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
1393
1823
  const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
1394
- const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
1395
1824
  // ─────────────────────────────────────────────────────────────────────────────
1396
1825
 
1397
1826
  // Rate limiter for /ask and /run — prevents rapid-fire Claude calls
@@ -1445,12 +1874,23 @@ function attachOrCreateSession(chatId, projCwd, name, engine) {
1445
1874
  * 主路径已迁移到 daemon-agent-tools.editAgentRoleDefinition。
1446
1875
  * 保留该实现仅用于兼容回退路径。
1447
1876
  */
1448
- async function mergeAgentRole(cwd, description) {
1877
+ async function mergeAgentRole(cwd, description, isClone = false, parentCwd = null) {
1449
1878
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
1450
1879
  // Sanitize user input: strip control chars, cap length to prevent prompt stuffing
1451
1880
  const safeDesc = String(description || '').replace(/[\x00-\x1F\x7F]/g, ' ').slice(0, 500);
1452
1881
  if (!fs.existsSync(claudeMdPath)) {
1453
- // 直接创建,无需调 Claude
1882
+ // 分身模式:symlink 到父 Agent 的 CLAUDE.md
1883
+ if (isClone) {
1884
+ const sourceCwd = parentCwd || path.dirname(cwd);
1885
+ const parentClaudeMd = path.join(sourceCwd, 'CLAUDE.md');
1886
+ if (fs.existsSync(parentClaudeMd)) {
1887
+ try {
1888
+ fs.symlinkSync(parentClaudeMd, claudeMdPath, 'file');
1889
+ return { created: true, symlinked: true };
1890
+ } catch { /* fall through to normal creation */ }
1891
+ }
1892
+ }
1893
+ // 普通模式:直接创建
1454
1894
  const content = `## Agent 角色\n\n${safeDesc}\n`;
1455
1895
  fs.writeFileSync(claudeMdPath, content, 'utf8');
1456
1896
  return { created: true };
@@ -1556,6 +1996,7 @@ async function doBindAgent(bot, chatId, agentName, agentCwd) {
1556
1996
  // ---------------------------------------------------------
1557
1997
  const {
1558
1998
  findSessionFile,
1999
+ findCodexSessionFile,
1559
2000
  clearSessionFileCache,
1560
2001
  truncateSessionToCheckpoint,
1561
2002
  listRecentSessions,
@@ -1565,15 +2006,17 @@ const {
1565
2006
  sessionRichLabel,
1566
2007
  getSessionRecentContext,
1567
2008
  buildSessionCardElements,
1568
- listProjectDirs,
1569
2009
  getSession,
1570
2010
  getSessionForEngine,
1571
2011
  createSession,
2012
+ restoreSessionFromReply,
1572
2013
  getSessionName,
1573
2014
  writeSessionName,
1574
2015
  markSessionStarted,
1575
2016
  watchSessionFiles,
1576
2017
  isEngineSessionValid,
2018
+ getCodexSessionSandboxProfile,
2019
+ getCodexSessionPermissionMode,
1577
2020
  } = createSessionStore({
1578
2021
  fs,
1579
2022
  path,
@@ -1726,6 +2169,8 @@ const getEngineRuntime = createEngineRuntimeFactory({
1726
2169
  getActiveProviderEnv,
1727
2170
  });
1728
2171
 
2172
+ let wakeRecoveryHook = null;
2173
+
1729
2174
  const {
1730
2175
  checkPrecondition,
1731
2176
  executeTask,
@@ -1754,6 +2199,7 @@ const {
1754
2199
  isInSleepMode: () => _inSleepMode,
1755
2200
  setSleepMode: (next) => { _inSleepMode = !!next; },
1756
2201
  spawnSessionSummaries,
2202
+ getWakeRecoveryHook: () => wakeRecoveryHook,
1757
2203
  skillEvolution,
1758
2204
  });
1759
2205
 
@@ -1765,6 +2211,10 @@ const pendingBinds = new Map(); // chatId -> agentName
1765
2211
  // chatId -> { step: 'dir'|'name'|'desc', dir: string, name: string }
1766
2212
  const pendingAgentFlows = new Map();
1767
2213
 
2214
+ // Pending /agent new team 多步向导状态机
2215
+ // chatId -> { step: 'name'|'members'|'cwd'|'creating', name, members, parentCwd }
2216
+ const pendingTeamFlows = new Map();
2217
+
1768
2218
  // Pending activation: after creating an agent with skipChatBinding=true,
1769
2219
  // store here so any new unbound group can activate it with /activate
1770
2220
  // { agentKey, agentName, cwd, createdAt }
@@ -1856,7 +2306,10 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
1856
2306
  sendFileButtons,
1857
2307
  findSessionFile,
1858
2308
  listRecentSessions,
2309
+ getSessionRecentContext,
1859
2310
  isEngineSessionValid,
2311
+ getCodexSessionSandboxProfile,
2312
+ getCodexSessionPermissionMode,
1860
2313
  getSession,
1861
2314
  getSessionForEngine,
1862
2315
  createSession,
@@ -1916,6 +2369,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
1916
2369
  sendBrowse,
1917
2370
  sendDirPicker,
1918
2371
  getSession,
2372
+ getSessionForEngine,
1919
2373
  listRecentSessions,
1920
2374
  buildSessionCardElements,
1921
2375
  sessionLabel,
@@ -1924,6 +2378,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
1924
2378
  getSessionRecentContext,
1925
2379
  pendingBinds,
1926
2380
  pendingAgentFlows,
2381
+ pendingTeamFlows,
1927
2382
  pendingActivations,
1928
2383
  doBindAgent,
1929
2384
  mergeAgentRole,
@@ -1932,6 +2387,9 @@ const { handleAgentCommand } = createAgentCommandHandler({
1932
2387
  agentFlowTtlMs: getAgentFlowTtlMs,
1933
2388
  agentBindTtlMs: getAgentBindTtlMs,
1934
2389
  getDefaultEngine,
2390
+ writeConfigSafe,
2391
+ backupConfig,
2392
+ execSync,
1935
2393
  });
1936
2394
 
1937
2395
  // Caffeinate process for /nosleep toggle (macOS only)
@@ -1953,11 +2411,14 @@ const { handleExecCommand } = createExecCommandHandler({
1953
2411
  loadState,
1954
2412
  saveState,
1955
2413
  getSession,
2414
+ getSessionForEngine,
1956
2415
  getSessionName,
1957
2416
  createSession,
1958
2417
  findSessionFile,
2418
+ findCodexSessionFile,
1959
2419
  loadConfig,
1960
2420
  getDistillModel,
2421
+ getDefaultEngine,
1961
2422
  });
1962
2423
 
1963
2424
  const { handleOpsCommand } = createOpsCommandHandler({
@@ -1966,9 +2427,12 @@ const { handleOpsCommand } = createOpsCommandHandler({
1966
2427
  spawn,
1967
2428
  execSync,
1968
2429
  log,
2430
+ loadConfig,
2431
+ loadState,
1969
2432
  messageQueue,
1970
2433
  activeProcesses,
1971
2434
  getSession,
2435
+ getSessionForEngine,
1972
2436
  listCheckpoints,
1973
2437
  cpDisplayLabel,
1974
2438
  truncateSessionToCheckpoint,
@@ -1979,6 +2443,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
1979
2443
  cleanupCheckpoints,
1980
2444
  getNoSleepProcess: () => caffeinateProcess,
1981
2445
  setNoSleepProcess: (p) => { caffeinateProcess = p || null; },
2446
+ getDefaultEngine,
1982
2447
  });
1983
2448
 
1984
2449
  const { handleCommand } = createCommandRouter({
@@ -2015,7 +2480,7 @@ setDispatchHandler(handleCommand);
2015
2480
  // ---------------------------------------------------------
2016
2481
  // BOT BRIDGES
2017
2482
  // ---------------------------------------------------------
2018
- const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
2483
+ const { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
2019
2484
  fs,
2020
2485
  path,
2021
2486
  HOME,
@@ -2025,10 +2490,13 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
2025
2490
  loadState,
2026
2491
  saveState,
2027
2492
  getSession,
2493
+ restoreSessionFromReply,
2028
2494
  handleCommand,
2029
2495
  pendingActivations,
2030
2496
  activeProcesses,
2031
2497
  messageQueue,
2498
+ sendRemoteDispatch,
2499
+ handleRemoteDispatchMessage,
2032
2500
  });
2033
2501
 
2034
2502
  const { killExistingDaemon, writePid, cleanPid } = createPidManager({
@@ -2147,7 +2615,7 @@ async function main() {
2147
2615
  }
2148
2616
 
2149
2617
  // Config validation: warn on unknown/suspect fields
2150
- const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
2618
+ const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
2151
2619
  const KNOWN_DAEMON = [
2152
2620
  'model', // legacy (still valid as fallback)
2153
2621
  'models', // per-engine model map: { claude, codex }
@@ -2238,6 +2706,7 @@ async function main() {
2238
2706
  // Bridges
2239
2707
  let telegramBridge = null;
2240
2708
  let feishuBridge = null;
2709
+ let lastWakeBridgeRecoveryAt = 0;
2241
2710
 
2242
2711
  const notifier = createNotifier({
2243
2712
  log,
@@ -2251,6 +2720,25 @@ async function main() {
2251
2720
  // Start dispatch socket server (low-latency IPC, fallback: file polling still works)
2252
2721
  const dispatchSocket = startDispatchSocket(() => config);
2253
2722
 
2723
+ wakeRecoveryHook = async ({ sleepSeconds }) => {
2724
+ const now = Date.now();
2725
+ if (now - lastWakeBridgeRecoveryAt < 60 * 1000) {
2726
+ log('INFO', `[WAKE-DETECT] bridge recovery skipped — cooldown active (${Math.round((now - lastWakeBridgeRecoveryAt) / 1000)}s since last)`);
2727
+ return;
2728
+ }
2729
+ lastWakeBridgeRecoveryAt = now;
2730
+ const tasks = [];
2731
+ if (telegramBridge && typeof telegramBridge.reconnect === 'function') {
2732
+ log('INFO', `[WAKE-DETECT] reconnecting Telegram bridge after ${sleepSeconds}s sleep`);
2733
+ tasks.push(Promise.resolve().then(() => telegramBridge.reconnect()));
2734
+ }
2735
+ if (feishuBridge && typeof feishuBridge.reconnect === 'function') {
2736
+ log('INFO', `[WAKE-DETECT] reconnecting Feishu bridge after ${sleepSeconds}s sleep`);
2737
+ tasks.push(Promise.resolve().then(() => feishuBridge.reconnect()));
2738
+ }
2739
+ await Promise.allSettled(tasks);
2740
+ };
2741
+
2254
2742
  // Start heartbeat scheduler
2255
2743
  let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
2256
2744
 
@@ -2303,6 +2791,11 @@ async function main() {
2303
2791
  // Reuse full shutdown logic, then self-spawn replacement.
2304
2792
  shutdown({ restartReason: 'daemon-script-changed' }).catch(() => process.exit(1));
2305
2793
  },
2794
+ // Agent soul layer auto-repair on config hot-reload
2795
+ repairAgentLayer,
2796
+ writeConfigSafe,
2797
+ expandPath,
2798
+ HOME,
2306
2799
  });
2307
2800
  // Expose reloadConfig to handleCommand via closure
2308
2801
  global._metameReload = runtimeWatchers.reloadConfig;
@@ -2310,6 +2803,8 @@ async function main() {
2310
2803
  // Start bridges (both can run simultaneously)
2311
2804
  telegramBridge = await startTelegramBridge(config, executeTaskByName);
2312
2805
  feishuBridge = await startFeishuBridge(config, executeTaskByName);
2806
+ await startImessageBridge(config, executeTaskByName);
2807
+ await startSiriBridge(config, executeTaskByName);
2313
2808
  if (feishuBridge) _dispatchBridgeRef = feishuBridge; // store bridge, not bot, so .bot stays live after reconnects
2314
2809
 
2315
2810
  // Notify once on startup (single message, no duplicates)
@@ -2356,6 +2851,8 @@ async function main() {
2356
2851
  try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
2357
2852
  // Kill all tracked engine process groups before exiting (covers sub-agents too)
2358
2853
  for (const [cid, proc] of activeProcesses) {
2854
+ proc.aborted = true;
2855
+ proc.abortReason = opts.restartReason ? 'daemon-restart' : 'shutdown';
2359
2856
  try { process.kill(-proc.child.pid, 'SIGKILL'); } catch { try { proc.child.kill('SIGKILL'); } catch { } }
2360
2857
  log('INFO', `Shutdown: killed engine process group for chatId ${cid}`);
2361
2858
  }
@@ -2369,6 +2866,10 @@ async function main() {
2369
2866
  process.exit(0);
2370
2867
  };
2371
2868
 
2869
+ process.on('SIGUSR2', () => {
2870
+ shutdown({ restartReason: process.env.METAME_DEPLOY_RESTART_REASON || 'external-restart' })
2871
+ .catch(() => process.exit(1));
2872
+ });
2372
2873
  process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
2373
2874
  process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
2374
2875
 
@@ -2429,4 +2930,21 @@ if (process.argv.includes('--run')) {
2429
2930
  }
2430
2931
 
2431
2932
  // Export for testing & cross-bot dispatch
2432
- module.exports = { executeTask, loadConfig, loadState, buildProfilePreamble, parseInterval, handleRemoteDispatchMessage, sendRemoteDispatch };
2933
+ module.exports = {
2934
+ executeTask,
2935
+ loadConfig,
2936
+ loadState,
2937
+ buildProfilePreamble,
2938
+ parseInterval,
2939
+ handleRemoteDispatchMessage,
2940
+ sendRemoteDispatch,
2941
+ __test: {
2942
+ buildDispatchPrompt,
2943
+ createStreamForwardBot,
2944
+ buildDispatchTaskCard,
2945
+ stripLeadingPlanSection,
2946
+ resolveDispatchTarget,
2947
+ resolveDispatchReadOnly,
2948
+ isMacLocalOrchestratorIntent,
2949
+ },
2950
+ };