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
@@ -20,7 +20,6 @@ function createCommandRouter(deps) {
20
20
  getNoSleepProcess,
21
21
  activeProcesses,
22
22
  messageQueue,
23
- sleep,
24
23
  log,
25
24
  agentTools,
26
25
  pendingAgentFlows,
@@ -29,6 +28,71 @@ function createCommandRouter(deps) {
29
28
  getDefaultEngine,
30
29
  } = deps;
31
30
 
31
+ function clearQueuedTimer(chatId) {
32
+ const q = messageQueue && messageQueue.get(chatId);
33
+ if (q && q.timer) {
34
+ clearTimeout(q.timer);
35
+ q.timer = null;
36
+ }
37
+ }
38
+
39
+ function interruptActiveProcess(chatId) {
40
+ const proc = activeProcesses.get(chatId);
41
+ if (proc && proc.child) {
42
+ proc.aborted = true;
43
+ const signal = proc.killSignal || 'SIGTERM';
44
+ try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function shouldPauseAndMergeFollowUps(chatId) {
51
+ const proc = activeProcesses.get(chatId);
52
+ return !!(proc && proc.engine === 'codex');
53
+ }
54
+
55
+ function getFollowUpDebounceMs(config) {
56
+ const raw = Number(config && config.daemon && config.daemon.follow_up_debounce_ms);
57
+ if (Number.isFinite(raw) && raw >= 300) return raw;
58
+ return 2500;
59
+ }
60
+
61
+ function buildMergedFollowUpPrompt(messages) {
62
+ return [
63
+ '继续上面的工作,并结合我刚刚连续补充的消息统一处理:',
64
+ '',
65
+ messages.join('\n'),
66
+ ].join('\n');
67
+ }
68
+
69
+ function scheduleQueuedResume(bot, chatId, config, readOnly, senderId) {
70
+ const q = messageQueue.get(chatId);
71
+ if (!q || q.mode !== 'resume-after-pause') return;
72
+ clearQueuedTimer(chatId);
73
+ const delay = getFollowUpDebounceMs(config);
74
+ q.timer = setTimeout(async () => {
75
+ const pending = messageQueue.get(chatId);
76
+ if (!pending || pending.mode !== 'resume-after-pause') return;
77
+ if (activeProcesses.has(chatId)) {
78
+ scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
79
+ return;
80
+ }
81
+ const msgs = pending.messages.splice(0);
82
+ messageQueue.delete(chatId);
83
+ if (msgs.length === 0) return;
84
+ log('INFO', `Follow-up: resuming with ${msgs.length} merged queued message(s) for ${chatId}`);
85
+ resetCooldown(chatId);
86
+ try {
87
+ await askClaude(bot, chatId, buildMergedFollowUpPrompt(msgs), config, readOnly, senderId);
88
+ } catch (err) {
89
+ log('WARN', `Follow-up resume failed for ${chatId}: ${err.message}`);
90
+ try { await bot.sendMessage(chatId, `⚠️ 继续处理补充消息失败:${err.message}`); } catch { /* */ }
91
+ }
92
+ }, delay);
93
+ if (typeof q.timer.unref === 'function') q.timer.unref();
94
+ }
95
+
32
96
  function resolveFlowTtlMs() {
33
97
  const raw = typeof agentFlowTtlMs === 'function' ? agentFlowTtlMs() : agentFlowTtlMs;
34
98
  const num = Number(raw);
@@ -214,10 +278,19 @@ function createCommandRouter(deps) {
214
278
  return null;
215
279
  }
216
280
 
281
+ function buildSessionChatId(chatId, projectKey = null) {
282
+ const rawChatId = String(chatId || '');
283
+ const inferredKey = projectKey || projectKeyFromVirtualChatId(rawChatId);
284
+ if (rawChatId.startsWith('_agent_') || rawChatId.startsWith('_scope_')) return rawChatId;
285
+ return inferredKey ? `_bound_${inferredKey}` : rawChatId;
286
+ }
287
+
217
288
  function getBoundProjectForChat(chatId, cfg) {
218
289
  const map = {
219
290
  ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
220
291
  ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
292
+ ...(cfg.imessage ? cfg.imessage.chat_agent_map : {}),
293
+ ...(cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
221
294
  };
222
295
  const key = map[String(chatId)];
223
296
  const proj = key && cfg.projects ? cfg.projects[key] : null;
@@ -350,12 +423,83 @@ function createCommandRouter(deps) {
350
423
  });
351
424
  }
352
425
 
426
+ function _detectCloneIntent(text) {
427
+ if (!text || text.startsWith('/') || text.length < 3) return false;
428
+ const cloneKeywords = ['分身', '再造', '克隆', '副本', '另一个自己', '另一个我'];
429
+ const hasCloneKeyword = cloneKeywords.some(k => text.includes(k));
430
+ if (hasCloneKeyword) {
431
+ const excludePatterns = [/已经/, /存在/, /有了/, /好了/, /完成/, /搞定/, /配置好/, /怎么建/, /如何建/, /方法/, /步骤/];
432
+ if (excludePatterns.some(p => p.test(text))) return false;
433
+ return true;
434
+ }
435
+ const actionKeywords = ['新建', '创建', '造', '做一个', '加一个', '增加', '添加'];
436
+ const hasAction = actionKeywords.some(k => text.includes(k));
437
+ if (hasAction && /分身|数字/.test(text)) return true;
438
+ if (/让.*做分身|叫.*做分身|甲.*做分身/.test(text)) return true;
439
+ return false;
440
+ }
441
+
442
+ function _detectNewAgentIntent(text) {
443
+ if (!text || text.startsWith('/') || text.length < 3) return false;
444
+ if (_detectCloneIntent(text)) return false;
445
+ if (_detectTeamIntent(text)) return false;
446
+ const agentKeywords = ['agent', '助手', '机器人', '小助手'];
447
+ const hasAgentKeyword = agentKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
448
+ const actionKeywords = ['新建', '创建', '造', '做一个', '加一个', '增加', '添加', '开一个'];
449
+ const hasAction = actionKeywords.some(k => text.includes(k));
450
+ if (hasAgentKeyword && hasAction) {
451
+ const excludePatterns = [/已经/, /存在/, /有了/, /好了/, /完成/, /搞定/, /配置好/, /怎么建/, /如何建/, /方法/, /步骤/, /是什么/, /哪个/];
452
+ if (excludePatterns.some(p => p.test(text))) return false;
453
+ return true;
454
+ }
455
+ if (/^(给我|帮我|我要|我想|给我加|帮我加)/.test(text) && hasAgentKeyword) return true;
456
+ return false;
457
+ }
458
+
459
+ function _detectTeamIntent(text) {
460
+ if (!text || text.startsWith('/') || text.length < 4) return false;
461
+ // Exclude: only mentioning team, no creation intent
462
+ if (/走team|用team|通过team|team里|team中|团队里|团队中|走团队|用团队|在team|在团队|team.*已经|团队.*已经|team.*讨论|团队.*讨论/.test(text)) return false;
463
+ // Positive match: team + action word
464
+ if ((text.includes('团队') || text.includes('工作组'))) {
465
+ if (/(新建|创建|造一个|加一个|组建|设置|建|搞)/.test(text)) {
466
+ if (/怎么|如何|方法|步骤/.test(text)) return false;
467
+ return true;
468
+ }
469
+ }
470
+ // Pattern: "建个团队" / "搞个团队"
471
+ if (/^(新建|创建|建|搞).*团队/.test(text)) return true;
472
+ return false;
473
+ }
474
+
353
475
  async function tryHandleAgentIntent(bot, chatId, text, config) {
354
476
  if (!agentTools || !text || text.startsWith('/')) return false;
355
477
  const key = String(chatId);
356
478
  if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
357
479
  const input = text.trim();
358
480
  if (!input) return false;
481
+
482
+ // Clone intent — route to /agent new clone wizard
483
+ if (_detectCloneIntent(input)) {
484
+ log('INFO', `[CloneIntent] "${input.slice(0, 80)}" → /agent new clone`);
485
+ await handleAgentCommand({ bot, chatId, text: '/agent new clone', config });
486
+ return true;
487
+ }
488
+
489
+ // New agent intent — route to /agent new wizard
490
+ if (_detectNewAgentIntent(input)) {
491
+ log('INFO', `[NewAgentIntent] "${input.slice(0, 80)}" → /agent new`);
492
+ await handleAgentCommand({ bot, chatId, text: '/agent new', config });
493
+ return true;
494
+ }
495
+
496
+ // Team creation intent — route to /agent new team wizard
497
+ if (_detectTeamIntent(input)) {
498
+ log('INFO', `[TeamIntent] "${input.slice(0, 80)}" → /agent new team`);
499
+ await handleAgentCommand({ bot, chatId, text: '/agent new team', config });
500
+ return true;
501
+ }
502
+
359
503
  const directAction = isLikelyDirectAgentAction(input);
360
504
  const issueReport = looksLikeAgentIssueReport(input);
361
505
  if (issueReport && !directAction) return false;
@@ -514,18 +658,28 @@ function createCommandRouter(deps) {
514
658
  // --- chat_agent_map: auto-switch agent based on dedicated chatId ---
515
659
  // Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
516
660
  // e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
517
- const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
661
+ const chatAgentMap = {
662
+ ...(config.telegram ? config.telegram.chat_agent_map : {}),
663
+ ...(config.feishu ? config.feishu.chat_agent_map : {}),
664
+ ...(config.imessage ? config.imessage.chat_agent_map : {}),
665
+ ...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
666
+ };
518
667
  const _chatIdStr = String(chatId);
519
668
  const mappedKey = chatAgentMap[_chatIdStr] ||
520
669
  projectKeyFromVirtualChatId(_chatIdStr);
521
670
  if (mappedKey && config.projects && config.projects[mappedKey]) {
522
671
  const proj = config.projects[mappedKey];
523
672
  const projCwd = normalizeCwd(proj.cwd);
524
- const cur = loadState().sessions?.[chatId];
525
- const curEngine = String((cur && cur.engine) || getDefaultEngine()).toLowerCase();
673
+ const sessionChatId = buildSessionChatId(chatId, mappedKey);
674
+ const cur = loadState().sessions?.[sessionChatId];
526
675
  const projEngine = String((proj && proj.engine) || getDefaultEngine()).toLowerCase();
527
- if (!cur || cur.cwd !== projCwd || curEngine !== projEngine) {
528
- attachOrCreateSession(chatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
676
+ // Multi-engine format stores engines in cur.engines object; legacy format uses cur.engine string.
677
+ // Check whether the session already has a slot for the project's configured engine.
678
+ const curHasEngine = cur && (
679
+ cur.engines ? !!cur.engines[projEngine] : String(cur.engine || '').toLowerCase() === projEngine
680
+ );
681
+ if (!cur || cur.cwd !== projCwd || !curHasEngine) {
682
+ attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey, proj.engine || getDefaultEngine());
529
683
  }
530
684
  }
531
685
 
@@ -538,7 +692,7 @@ function createCommandRouter(deps) {
538
692
  return;
539
693
  }
540
694
 
541
- const adminResult = await handleAdminCommand({ bot, chatId, text, config, state });
695
+ const adminResult = await handleAdminCommand({ bot, chatId, text, config, state, senderId });
542
696
  if (adminResult.handled) {
543
697
  config = adminResult.config || config;
544
698
  return;
@@ -602,16 +756,10 @@ function createCommandRouter(deps) {
602
756
  if (activeProcesses.has(chatId) && INTERRUPT_RE.test(text.trim())) {
603
757
  // Kill current process but preserve session for resume
604
758
  if (messageQueue.has(chatId)) {
605
- const q = messageQueue.get(chatId);
606
- if (q.timer) clearTimeout(q.timer);
759
+ clearQueuedTimer(chatId);
607
760
  messageQueue.delete(chatId);
608
761
  }
609
- const proc = activeProcesses.get(chatId);
610
- if (proc && proc.child) {
611
- proc.aborted = true;
612
- const signal = proc.killSignal || 'SIGTERM';
613
- try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
614
- }
762
+ interruptActiveProcess(chatId);
615
763
  await bot.sendMessage(chatId, '⏸ 好的,听你说');
616
764
  return;
617
765
  }
@@ -624,31 +772,60 @@ function createCommandRouter(deps) {
624
772
  if (handled) {
625
773
  // /last attached the session — now send "继续" to actually resume the conversation
626
774
  resetCooldown(chatId);
627
- await askClaude(bot, chatId, '继续上面的工作', config, readOnly);
775
+ await askClaude(bot, chatId, '继续上面的工作', config, readOnly, senderId);
628
776
  return;
629
777
  }
630
778
  // No session found — fall through to normal askClaude
631
779
  }
632
780
 
633
- // If a task is running: queue message, DON'T kill will be sent as follow-up after completion
781
+ // While collecting follow-up messages after a pause, keep merging them until the debounce window closes.
782
+ if (messageQueue.has(chatId)) {
783
+ const q = messageQueue.get(chatId);
784
+ if (q && q.mode === 'resume-after-pause') {
785
+ q.messages.push(text);
786
+ scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
787
+ return;
788
+ }
789
+ }
790
+
791
+ // If a task is running: pause it, collect the user's burst of messages, then resume with a merged follow-up.
634
792
  if (activeProcesses.has(chatId)) {
793
+ if (!shouldPauseAndMergeFollowUps(chatId)) {
794
+ const isFirst = !messageQueue.has(chatId);
795
+ if (isFirst) {
796
+ messageQueue.set(chatId, { messages: [] });
797
+ }
798
+ const q = messageQueue.get(chatId);
799
+ if (q.messages.length >= 10) {
800
+ await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
801
+ return;
802
+ }
803
+ q.messages.push(text);
804
+ if (isFirst) {
805
+ await bot.sendMessage(chatId, '📝 收到,完成后继续处理');
806
+ }
807
+ return;
808
+ }
635
809
  const isFirst = !messageQueue.has(chatId);
636
810
  if (isFirst) {
637
- messageQueue.set(chatId, { messages: [] });
811
+ messageQueue.set(chatId, { messages: [], mode: 'resume-after-pause', timer: null });
638
812
  }
639
813
  const q = messageQueue.get(chatId);
640
- if (q.messages.length >= 10) {
641
- await bot.sendMessage(chatId, '⚠️ 排队已满(10条),请等当前任务完成');
642
- return;
643
- }
644
814
  q.messages.push(text);
645
815
  if (isFirst) {
646
- await bot.sendMessage(chatId, '📝 收到,完成后继续处理');
816
+ interruptActiveProcess(chatId);
817
+ await bot.sendMessage(chatId, '⏸ 已暂停当前任务,你可以继续连发,我会自动合并后续内容再继续');
647
818
  }
819
+ scheduleQueuedResume(bot, chatId, config, readOnly, senderId);
648
820
  return;
649
821
  }
650
822
  // Strict mode: chats with a fixed agent in chat_agent_map must not cross-dispatch
651
- const _strictChatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
823
+ const _strictChatAgentMap = {
824
+ ...(config.telegram ? config.telegram.chat_agent_map : {}),
825
+ ...(config.feishu ? config.feishu.chat_agent_map : {}),
826
+ ...(config.imessage ? config.imessage.chat_agent_map : {}),
827
+ ...(config.siri_bridge ? config.siri_bridge.chat_agent_map : {}),
828
+ };
652
829
  const _isStrictChat = !!(_strictChatAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
653
830
 
654
831
  // Nickname-only switch: bypass cooldown + budget (no Claude call)
@@ -658,7 +835,7 @@ function createCommandRouter(deps) {
658
835
  if (quickAgent && !quickAgent.rest) {
659
836
  const { key, proj } = quickAgent;
660
837
  const projCwd = normalizeCwd(proj.cwd);
661
- attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine || getDefaultEngine());
838
+ attachOrCreateSession(buildSessionChatId(chatId, key), projCwd, proj.name || key, proj.engine || getDefaultEngine());
662
839
  log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
663
840
  await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
664
841
  return;
@@ -684,7 +861,7 @@ function createCommandRouter(deps) {
684
861
  await bot.sendMessage(chatId, 'Daily token budget exceeded.');
685
862
  return;
686
863
  }
687
- const claudeResult = await askClaude(bot, chatId, text, config, readOnly);
864
+ const claudeResult = await askClaude(bot, chatId, text, config, readOnly, senderId);
688
865
  const claudeFailed = !!(claudeResult && claudeResult.ok === false);
689
866
  const claudeAborted = !!(claudeResult && claudeResult.error === 'Stopped by user');
690
867
  if (claudeFailed && !claudeAborted && !macLocalFirst && macFallbackEnabled && allowLocalMacControl) {
@@ -702,13 +879,14 @@ function createCommandRouter(deps) {
702
879
  // Use while-loop instead of recursion to avoid unbounded stack growth
703
880
  while (messageQueue.has(chatId)) {
704
881
  const q = messageQueue.get(chatId);
882
+ if (q && q.mode === 'resume-after-pause') break;
705
883
  const msgs = q.messages.splice(0);
706
884
  messageQueue.delete(chatId);
707
885
  if (msgs.length === 0) break;
708
886
  const combined = msgs.join('\n');
709
887
  log('INFO', `Follow-up: processing ${msgs.length} queued message(s) for ${chatId}`);
710
888
  resetCooldown(chatId);
711
- const followUp = await askClaude(bot, chatId, combined, config, readOnly);
889
+ const followUp = await askClaude(bot, chatId, combined, config, readOnly, senderId);
712
890
  if (followUp && followUp.error === 'Stopped by user') break;
713
891
  }
714
892
 
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ function createCommandSessionResolver(deps) {
4
+ const {
5
+ path,
6
+ loadConfig,
7
+ loadState,
8
+ getSession,
9
+ getSessionForEngine,
10
+ getDefaultEngine = () => 'claude',
11
+ } = deps;
12
+
13
+ function normalizeEngineName(name) {
14
+ return String(name || '').trim().toLowerCase() === 'codex' ? 'codex' : getDefaultEngine();
15
+ }
16
+
17
+ function inferStoredEngine(rawSession) {
18
+ if (!rawSession || typeof rawSession !== 'object') return getDefaultEngine();
19
+ if (rawSession.engine) return normalizeEngineName(rawSession.engine);
20
+ const slots = rawSession.engines && typeof rawSession.engines === 'object' ? rawSession.engines : null;
21
+ if (!slots) return getDefaultEngine();
22
+ const started = Object.entries(slots).find(([, slot]) => slot && slot.started);
23
+ if (started) return normalizeEngineName(started[0]);
24
+ const available = Object.keys(slots);
25
+ return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
26
+ }
27
+
28
+ function buildBoundSessionChatId(projectKey) {
29
+ const key = String(projectKey || '').trim();
30
+ return key ? `_bound_${key}` : '';
31
+ }
32
+
33
+ function normalizeRouteCwd(cwd) {
34
+ if (!cwd) return null;
35
+ try {
36
+ return path.resolve(String(cwd));
37
+ } catch {
38
+ return String(cwd);
39
+ }
40
+ }
41
+
42
+ function getSessionRoute(chatId) {
43
+ const cfg = loadConfig();
44
+ const state = loadState();
45
+ const chatKey = String(chatId);
46
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
47
+ const boundKey = agentMap[chatKey] || null;
48
+ const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
49
+ const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
50
+ const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
51
+ ? boundProj.team.find((m) => m && m.key === stickyKey)
52
+ : null;
53
+
54
+ if (stickyMember) {
55
+ return {
56
+ sessionChatId: `_agent_${stickyMember.key}`,
57
+ cwd: normalizeRouteCwd(stickyMember.cwd || (boundProj && boundProj.cwd) || null),
58
+ engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
59
+ };
60
+ }
61
+
62
+ if (boundProj) {
63
+ return {
64
+ sessionChatId: buildBoundSessionChatId(boundKey),
65
+ cwd: normalizeRouteCwd(boundProj.cwd || null),
66
+ engine: normalizeEngineName(boundProj.engine),
67
+ };
68
+ }
69
+
70
+ const rawSession = getSession(chatId);
71
+ return {
72
+ sessionChatId: String(chatId),
73
+ cwd: rawSession && rawSession.cwd ? normalizeRouteCwd(rawSession.cwd) : null,
74
+ engine: inferStoredEngine(rawSession),
75
+ };
76
+ }
77
+
78
+ function getActiveSession(chatId) {
79
+ const route = getSessionRoute(chatId);
80
+ const rawSession = getSession(route.sessionChatId) || getSession(chatId);
81
+ const engine = normalizeEngineName((rawSession && rawSession.engine) || route.engine);
82
+ const engineSession = getSessionForEngine(route.sessionChatId, engine)
83
+ || getSessionForEngine(chatId, engine);
84
+ if (engineSession && engineSession.id) {
85
+ return {
86
+ route,
87
+ sessionKey: route.sessionChatId,
88
+ engine,
89
+ session: engineSession,
90
+ };
91
+ }
92
+ if (rawSession && rawSession.id) {
93
+ return {
94
+ route,
95
+ sessionKey: route.sessionChatId,
96
+ engine,
97
+ session: { cwd: rawSession.cwd, engine, id: rawSession.id, started: !!rawSession.started },
98
+ };
99
+ }
100
+ return {
101
+ route,
102
+ sessionKey: route.sessionChatId,
103
+ engine,
104
+ session: null,
105
+ };
106
+ }
107
+
108
+ return {
109
+ normalizeEngineName,
110
+ inferStoredEngine,
111
+ buildBoundSessionChatId,
112
+ normalizeRouteCwd,
113
+ getSessionRoute,
114
+ getActiveSession,
115
+ };
116
+ }
117
+
118
+ module.exports = { createCommandSessionResolver };
@@ -4,13 +4,20 @@
4
4
  telegram:
5
5
  enabled: false
6
6
  bot_token: null
7
+ admin_chat_id: null
7
8
  allowed_chat_ids: []
8
9
 
9
10
  feishu:
10
11
  enabled: false
11
12
  app_id: null
12
13
  app_secret: null
14
+ admin_chat_id: null
13
15
  allowed_chat_ids: []
16
+ remote_dispatch:
17
+ enabled: false
18
+ self: ""
19
+ chat_id: ""
20
+ secret: ""
14
21
 
15
22
  projects:
16
23
  # Per-project heartbeat tasks. Each project's tasks are isolated and
@@ -239,7 +239,7 @@ function parseCodexStreamEvent(line) {
239
239
  }
240
240
 
241
241
  function buildClaudeArgs(options = {}) {
242
- const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, daemonCfg = {}, session = {} } = options;
242
+ const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {} } = options;
243
243
  const args = ['-p', '--model', model];
244
244
  if (readOnly) {
245
245
  const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
@@ -260,8 +260,76 @@ function buildClaudeArgs(options = {}) {
260
260
  return args;
261
261
  }
262
262
 
263
+ function normalizeCodexSandboxMode(value, fallback = 'danger-full-access') {
264
+ const text = String(value || '').trim().toLowerCase();
265
+ if (!text) return fallback;
266
+ if (text === 'read-only' || text === 'readonly') return 'read-only';
267
+ if (text === 'workspace-write' || text === 'workspace') return 'workspace-write';
268
+ if (
269
+ text === 'danger-full-access'
270
+ || text === 'dangerous'
271
+ || text === 'full-access'
272
+ || text === 'full'
273
+ || text === 'bypass'
274
+ || text === 'writable'
275
+ ) return 'danger-full-access';
276
+ return fallback;
277
+ }
278
+
279
+ function normalizeCodexApprovalPolicy(value, fallback = 'never') {
280
+ const text = String(value || '').trim().toLowerCase();
281
+ if (!text) return fallback;
282
+ if (text === 'never' || text === 'no' || text === 'none') return 'never';
283
+ if (text === 'on-failure' || text === 'on_failure' || text === 'failure') return 'on-failure';
284
+ if (text === 'on-request' || text === 'on_request' || text === 'request') return 'on-request';
285
+ if (text === 'untrusted') return 'untrusted';
286
+ return fallback;
287
+ }
288
+
289
+ function resolveCodexPermissionProfile(options = {}) {
290
+ const { readOnly = false, daemonCfg = {}, session = {} } = options;
291
+ if (readOnly) {
292
+ return {
293
+ sandboxMode: 'read-only',
294
+ approvalPolicy: 'never',
295
+ permissionMode: 'read-only',
296
+ };
297
+ }
298
+
299
+ const codexCfg = (daemonCfg && daemonCfg.codex && typeof daemonCfg.codex === 'object') ? daemonCfg.codex : {};
300
+ const sandboxMode = normalizeCodexSandboxMode(
301
+ codexCfg.sandbox_mode
302
+ || codexCfg.sandboxMode
303
+ || codexCfg.sandbox
304
+ || codexCfg.permission_mode
305
+ || codexCfg.permissionMode
306
+ || session.sandboxMode
307
+ || session.permissionMode,
308
+ 'danger-full-access'
309
+ );
310
+ const approvalPolicy = normalizeCodexApprovalPolicy(
311
+ codexCfg.approval_policy
312
+ || codexCfg.approvalPolicy
313
+ || session.approvalPolicy,
314
+ sandboxMode === 'danger-full-access' ? 'never' : 'on-failure'
315
+ );
316
+
317
+ return {
318
+ sandboxMode,
319
+ approvalPolicy,
320
+ permissionMode: sandboxMode,
321
+ };
322
+ }
323
+
263
324
  function buildCodexArgs(options = {}) {
264
- const { model = ENGINE_MODEL_CONFIG.codex.main, readOnly = false, daemonCfg = {}, session = {}, cwd } = options;
325
+ const {
326
+ model = ENGINE_MODEL_CONFIG.codex.main,
327
+ readOnly = false,
328
+ daemonCfg = {},
329
+ session = {},
330
+ cwd,
331
+ permissionProfile = null,
332
+ } = options;
265
333
  const isResume = (session && session.started && session.id && session.id !== '__continue__');
266
334
  const args = isResume
267
335
  ? ['exec', 'resume', session.id]
@@ -272,16 +340,13 @@ function buildCodexArgs(options = {}) {
272
340
  // -C (cwd) is only supported on fresh exec, not resume
273
341
  if (cwd && !isResume) args.push('-C', cwd);
274
342
 
275
- // Permission flags are only valid on fresh exec, not resume.
276
- // `codex exec resume` does not accept -s or --dangerously-bypass-approvals-and-sandbox.
277
- if (!isResume) {
278
- if (readOnly) {
279
- args.push('-s', 'read-only');
280
- } else {
281
- // Mobile sessions: user cannot click permission dialogs.
282
- // Security relies on allowed_chat_ids whitelist, not tool restrictions.
283
- args.push('--dangerously-bypass-approvals-and-sandbox');
284
- }
343
+ const effectivePermissionProfile = permissionProfile || resolveCodexPermissionProfile({ readOnly, daemonCfg, session });
344
+ if (effectivePermissionProfile.sandboxMode === 'danger-full-access' && effectivePermissionProfile.approvalPolicy === 'never') {
345
+ // Keep the legacy shortcut for the fully-trusted mobile/default path.
346
+ args.push('--dangerously-bypass-approvals-and-sandbox');
347
+ } else {
348
+ // codex 0.114.0 removed --ask-for-approval; only -s <sandboxMode> is needed
349
+ args.push('-s', effectivePermissionProfile.sandboxMode);
285
350
  }
286
351
 
287
352
  // "-" means prompt is read from stdin.
@@ -289,6 +354,18 @@ function buildCodexArgs(options = {}) {
289
354
  return args;
290
355
  }
291
356
 
357
+ function buildCodexEnv(baseEnv = {}, { metameProject = '', metameSenderId = '' } = {}) {
358
+ const env = { ...baseEnv, METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
359
+ const strippedKeys = [
360
+ 'CODEX_THREAD_ID',
361
+ 'METAME_ACTIVE_SESSION',
362
+ 'CLAUDE_CODE_SSE_PORT',
363
+ ];
364
+ for (const key of strippedKeys) delete env[key];
365
+ if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
366
+ return env;
367
+ }
368
+
292
369
  function createEngineRuntimeFactory(deps = {}) {
293
370
  const home = deps.HOME || os.homedir();
294
371
  const claudeBin = deps.CLAUDE_BIN || resolveBinary('claude', { ...deps, HOME: home });
@@ -308,12 +385,7 @@ function createEngineRuntimeFactory(deps = {}) {
308
385
  killSignal: 'SIGTERM',
309
386
  timeouts: { idleMs: 10 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
310
387
  buildArgs: buildCodexArgs,
311
- buildEnv: ({ metameProject = '' } = {}) => {
312
- const env = { ...process.env, METAME_PROJECT: metameProject };
313
- // Unset CODEX_HOME if it points to a non-existent path (corrupted env var)
314
- if (env.CODEX_HOME && !fs.existsSync(env.CODEX_HOME)) delete env.CODEX_HOME;
315
- return env;
316
- },
388
+ buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => buildCodexEnv(process.env, { metameProject, metameSenderId }),
317
389
  parseStreamEvent: parseCodexStreamEvent,
318
390
  classifyError: classifyEngineError,
319
391
  };
@@ -326,9 +398,9 @@ function createEngineRuntimeFactory(deps = {}) {
326
398
  killSignal: 'SIGTERM',
327
399
  timeouts: { idleMs: 5 * 60 * 1000, toolMs: 25 * 60 * 1000, ceilingMs: 60 * 60 * 1000 },
328
400
  buildArgs: buildClaudeArgs,
329
- buildEnv: ({ metameProject = '' } = {}) => ({
401
+ buildEnv: ({ metameProject = '', metameSenderId = '' } = {}) => ({
330
402
  ...(() => {
331
- const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject };
403
+ const env = { ...process.env, ...getActiveProviderEnv(), METAME_PROJECT: metameProject, METAME_SENDER_ID: String(metameSenderId || '') };
332
404
  delete env.CLAUDECODE;
333
405
  return env;
334
406
  })(),
@@ -354,6 +426,10 @@ module.exports = {
354
426
  parseCodexStreamEvent,
355
427
  buildClaudeArgs,
356
428
  buildCodexArgs,
429
+ buildCodexEnv,
430
+ normalizeCodexSandboxMode,
431
+ normalizeCodexApprovalPolicy,
432
+ resolveCodexPermissionProfile,
357
433
  BUILTIN_CLAUDE_MODEL_VALUES,
358
434
  },
359
435
  };