metame-cli 1.5.10 → 1.5.12

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 (67) hide show
  1. package/README.md +49 -6
  2. package/index.js +266 -72
  3. package/package.json +7 -3
  4. package/scripts/daemon-admin-commands.js +34 -0
  5. package/scripts/daemon-agent-commands.js +6 -2
  6. package/scripts/daemon-bridges.js +41 -10
  7. package/scripts/daemon-claude-engine.js +128 -29
  8. package/scripts/daemon-command-router.js +16 -0
  9. package/scripts/daemon-command-session-route.js +3 -1
  10. package/scripts/daemon-default.yaml +3 -1
  11. package/scripts/daemon-engine-runtime.js +1 -5
  12. package/scripts/daemon-message-pipeline.js +113 -44
  13. package/scripts/daemon-ops-commands.js +25 -11
  14. package/scripts/daemon-reactive-lifecycle.js +757 -76
  15. package/scripts/daemon-session-commands.js +3 -2
  16. package/scripts/daemon-session-store.js +82 -27
  17. package/scripts/daemon-team-dispatch.js +21 -5
  18. package/scripts/daemon-utils.js +3 -1
  19. package/scripts/daemon.js +80 -2
  20. package/scripts/distill.js +1 -1
  21. package/scripts/docs/file-transfer.md +1 -0
  22. package/scripts/docs/maintenance-manual.md +55 -2
  23. package/scripts/docs/pointer-map.md +34 -0
  24. package/scripts/feishu-adapter.js +25 -0
  25. package/scripts/hooks/intent-file-transfer.js +2 -1
  26. package/scripts/hooks/intent-perpetual.js +109 -0
  27. package/scripts/hooks/intent-research.js +112 -0
  28. package/scripts/intent-registry.js +4 -0
  29. package/scripts/memory-extract.js +29 -1
  30. package/scripts/memory-nightly-reflect.js +104 -0
  31. package/scripts/ops-mission-queue.js +258 -0
  32. package/scripts/ops-verifier.js +197 -0
  33. package/scripts/signal-capture.js +3 -3
  34. package/scripts/skill-evolution.js +11 -2
  35. package/skills/agent-browser/SKILL.md +153 -0
  36. package/skills/agent-reach/SKILL.md +66 -0
  37. package/skills/agent-reach/evolution.json +13 -0
  38. package/skills/deep-research/SKILL.md +77 -0
  39. package/skills/find-skills/SKILL.md +133 -0
  40. package/skills/heartbeat-task-manager/SKILL.md +63 -0
  41. package/skills/macos-local-orchestrator/SKILL.md +192 -0
  42. package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
  43. package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
  44. package/skills/macos-mail-calendar/SKILL.md +394 -0
  45. package/skills/mcp-installer/SKILL.md +138 -0
  46. package/skills/skill-creator/LICENSE.txt +202 -0
  47. package/skills/skill-creator/README.md +72 -0
  48. package/skills/skill-creator/SKILL.md +96 -0
  49. package/skills/skill-creator/evolution.json +6 -0
  50. package/skills/skill-creator/references/creation-guide.md +116 -0
  51. package/skills/skill-creator/references/evolution-guide.md +74 -0
  52. package/skills/skill-creator/references/output-patterns.md +82 -0
  53. package/skills/skill-creator/references/workflows.md +28 -0
  54. package/skills/skill-creator/scripts/align_all.py +32 -0
  55. package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
  56. package/skills/skill-creator/scripts/init_skill.py +303 -0
  57. package/skills/skill-creator/scripts/merge_evolution.py +70 -0
  58. package/skills/skill-creator/scripts/package_skill.py +110 -0
  59. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  60. package/skills/skill-creator/scripts/setup.py +141 -0
  61. package/skills/skill-creator/scripts/smart_stitch.py +82 -0
  62. package/skills/skill-manager/SKILL.md +112 -0
  63. package/skills/skill-manager/scripts/delete_skill.py +31 -0
  64. package/skills/skill-manager/scripts/list_skills.py +61 -0
  65. package/skills/skill-manager/scripts/scan_and_check.py +125 -0
  66. package/skills/skill-manager/scripts/sync_index.py +144 -0
  67. package/skills/skill-manager/scripts/update_helper.py +39 -0
@@ -652,13 +652,30 @@ function createBridgeStarter(deps) {
652
652
  async function startFeishuBridge(config, executeTaskByName) {
653
653
  if (!config.feishu || !config.feishu.enabled) return null;
654
654
  if (!config.feishu.app_id || !config.feishu.app_secret) {
655
- log('WARN', 'Feishu enabled but app_id/app_secret missing');
655
+ log('ERROR', 'Feishu enabled but app_id/app_secret missing — bridge will NOT start. Check ~/.metame/daemon.yaml');
656
656
  return null;
657
657
  }
658
658
 
659
659
  const { createBot } = require('./feishu-adapter.js');
660
660
  const bot = createBot(config.feishu);
661
661
 
662
+ // Validate credentials before starting WebSocket — fail loud, not silent
663
+ try {
664
+ const validation = await bot.validateCredentials();
665
+ if (!validation.ok) {
666
+ log('ERROR', `Feishu credential check FAILED: ${validation.error}`);
667
+ if (validation.isAuthError) {
668
+ log('ERROR', 'Feishu bridge will NOT start — fix app_id/app_secret in ~/.metame/daemon.yaml and restart daemon');
669
+ return null;
670
+ }
671
+ log('WARN', 'Feishu credential check failed (possibly network issue) — attempting to start anyway');
672
+ } else {
673
+ log('INFO', 'Feishu credentials validated OK');
674
+ }
675
+ } catch (e) {
676
+ log('WARN', `Feishu credential pre-check error: ${e.message} — attempting to start anyway`);
677
+ }
678
+
662
679
  try {
663
680
  const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
664
681
  const liveCfg = loadConfig();
@@ -750,6 +767,7 @@ function createBridgeStarter(deps) {
750
767
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
751
768
  const parentId = extractFeishuReplyMessageId(event);
752
769
  let _replyAgentKey = null;
770
+ let _replyMappingFound = false; // true = mapping exists (agentKey may be null = main)
753
771
  // Load state once for the entire routing block
754
772
  const _st = loadState();
755
773
  if (parentId) {
@@ -758,6 +776,7 @@ function createBridgeStarter(deps) {
758
776
  if (parentId) {
759
777
  const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
760
778
  if (mapped) {
779
+ _replyMappingFound = true;
761
780
  if (typeof restoreSessionFromReply === 'function') {
762
781
  restoreSessionFromReply(chatId, mapped);
763
782
  } else {
@@ -835,16 +854,28 @@ function createBridgeStarter(deps) {
835
854
  // Bare /stop, no sticky set → fall through to handleCommand
836
855
  }
837
856
 
838
- // 0. Quoted reply → force route + set sticky
839
- if (_replyAgentKey) {
840
- const member = _boundProj.team.find(m => m.key === _replyAgentKey);
841
- if (member) {
842
- _setSticky(member.key);
843
- log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
844
- _dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
845
- return;
857
+ // 0. Quoted reply → force route based on which agent sent the parent message.
858
+ // Cases:
859
+ // a) agentKey = known team member route to that member (set sticky)
860
+ // b) agentKey = null, mapping found → user replied to main; clear sticky, route to main
861
+ // c) parentId present, no mapping → intent is explicit, avoid sticky; clear sticky, route to main
862
+ if (parentId) {
863
+ if (_replyAgentKey) {
864
+ const member = _boundProj.team.find(m => m.key === _replyAgentKey);
865
+ if (member) {
866
+ _setSticky(member.key);
867
+ log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
868
+ _dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
869
+ return;
870
+ }
871
+ // agentKey set but not a current team member → fall through to main
872
+ log('INFO', `Quoted reply agentKey=${_replyAgentKey} not in team, routing to main`);
846
873
  }
847
- log('INFO', `Quoted reply agentKey=${_replyAgentKey} not in team, falling through`);
874
+ // Cases b & c: no agentKey (main agent) or stale/unknown agentKey
875
+ _clearSticky();
876
+ log('INFO', `Quoted reply → route to main (agentKey=${_replyAgentKey} mappingFound=${_replyMappingFound})`);
877
+ await pipeline.processMessage(chatId, trimmedText, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
878
+ return;
848
879
  }
849
880
  // 1. Explicit nickname → route + set sticky
850
881
  const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
@@ -106,7 +106,15 @@ function createClaudeEngine(deps) {
106
106
  }
107
107
  // Card reuse for merge-pause: when a task is paused for message merging,
108
108
  // save the statusMsgId so the next askClaude reuses the same card.
109
- const _pausedCards = new Map(); // chatId -> { statusMsgId, cardHeader }
109
+ // Entries auto-expire via periodic sweep (60s) to prevent unbounded growth.
110
+ const _pausedCards = new Map(); // chatId -> { statusMsgId, cardHeader, savedAt }
111
+ const _PAUSED_CARD_TTL = 60000;
112
+ setInterval(() => {
113
+ const now = Date.now();
114
+ for (const [k, v] of _pausedCards) {
115
+ if (now - (v.savedAt || 0) > _PAUSED_CARD_TTL) _pausedCards.delete(k);
116
+ }
117
+ }, _PAUSED_CARD_TTL).unref();
110
118
 
111
119
  let mentorEngine = null;
112
120
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
@@ -239,6 +247,7 @@ function createClaudeEngine(deps) {
239
247
  && !!error
240
248
  && (!output || !!errorCode)
241
249
  && failureKind !== 'user-stop'
250
+ && failureKind !== 'merge-pause'
242
251
  && !!canRetry;
243
252
  }
244
253
 
@@ -481,6 +490,13 @@ function createClaudeEngine(deps) {
481
490
  retryPromptPrefix: '',
482
491
  };
483
492
  }
493
+ if (code === 'INTERRUPTED_MERGE_PAUSE' || lowered.includes('paused for merge')) {
494
+ return {
495
+ kind: 'merge-pause',
496
+ userMessage: '',
497
+ retryPromptPrefix: '',
498
+ };
499
+ }
484
500
  const interrupted = (
485
501
  lowered.includes('stopped by user')
486
502
  || lowered.includes('interrupted')
@@ -1275,9 +1291,9 @@ function createClaudeEngine(deps) {
1275
1291
  };
1276
1292
  const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
1277
1293
  const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
1278
- // _ackCardHeader: non-null for agents with icon/name (team members, dispatch); passed to editMessage to preserve header on streaming edits
1279
- let _ackCardHeader = (_ackBoundProj && _ackBoundProj.icon && _ackBoundProj.name)
1280
- ? { title: `${_ackBoundProj.icon} ${_ackBoundProj.name}`, color: _ackBoundProj.color || 'blue' }
1294
+ // _ackCardHeader: non-null for bound projects with a name; passed to editMessage to preserve header on streaming edits
1295
+ let _ackCardHeader = (_ackBoundProj && _ackBoundProj.name)
1296
+ ? { title: `${_ackBoundProj.icon || '🤖'} ${_ackBoundProj.name}`, color: _ackBoundProj.color || 'blue' }
1281
1297
  : null;
1282
1298
  // Reuse card from a paused merge (same card, no new push)
1283
1299
  const _pausedCard = _pausedCards.get(chatId);
@@ -1287,7 +1303,6 @@ function createClaudeEngine(deps) {
1287
1303
  const cardAge = _pausedCard.savedAt ? Date.now() - _pausedCard.savedAt : 0;
1288
1304
  if (cardAge > 30000) {
1289
1305
  log('INFO', `[askClaude] Discarding stale paused card for ${chatId} (${Math.round(cardAge / 1000)}s old)`);
1290
- if (_pausedCard.statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, _pausedCard.statusMsgId).catch(() => {});
1291
1306
  } else {
1292
1307
  statusMsgId = _pausedCard.statusMsgId;
1293
1308
  if (_pausedCard.cardHeader) _ackCardHeader = _pausedCard.cardHeader;
@@ -1365,6 +1380,10 @@ function createClaudeEngine(deps) {
1365
1380
  const sessionRaw = getSession(sessionChatId);
1366
1381
  const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
1367
1382
  const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
1383
+ // effectiveCwd: single source of truth for this request's working directory.
1384
+ // For bound projects, config always wins over stored session cwd.
1385
+ // Resolved once here; all downstream createSession/spawn calls use this.
1386
+ let effectiveCwd = boundCwd || null;
1368
1387
 
1369
1388
  // Engine is determined from config only — bound agent config wins, then global default.
1370
1389
  const engineName = normalizeEngineName(
@@ -1406,6 +1425,14 @@ function createClaudeEngine(deps) {
1406
1425
  let session = resolveSessionForEngine(sessionChatId, engineName) || { cwd: boundCwd || HOME, engine: engineName, id: null, started: false };
1407
1426
  session.engine = engineName; // keep local copy for Codex resume detection below
1408
1427
  session.logicalChatId = sessionChatId;
1428
+ // Finalize effectiveCwd: bound config > stored session > HOME
1429
+ if (!effectiveCwd) effectiveCwd = (session && session.cwd) || HOME;
1430
+ // Correct stored cwd if it drifted from config (e.g., stale state from prior bug)
1431
+ if (session.cwd !== effectiveCwd) {
1432
+ log('WARN', `[SessionCwd] correcting session cwd for ${sessionChatId}: ${session.cwd || 'unknown'} -> ${effectiveCwd}`);
1433
+ session = { ...session, cwd: effectiveCwd };
1434
+ await patchSessionSerialized(sessionChatId, (cur) => ({ ...cur, cwd: effectiveCwd }));
1435
+ }
1409
1436
 
1410
1437
  // Warm pool: check if a persistent process is available for this session (Claude only).
1411
1438
  // Declared early so downstream logic can skip expensive operations when reusing warm process.
@@ -1425,7 +1452,7 @@ function createClaudeEngine(deps) {
1425
1452
  }
1426
1453
  session = createSession(
1427
1454
  sessionChatId,
1428
- session.cwd,
1455
+ effectiveCwd,
1429
1456
  boundProject && boundProject.name ? boundProject.name : '',
1430
1457
  engineName,
1431
1458
  engineName === 'codex' ? requestedCodexPermissionProfile : undefined
@@ -1492,7 +1519,7 @@ function createClaudeEngine(deps) {
1492
1519
  const resumeInspection = inspectClaudeResumeSession(session, model);
1493
1520
  if (resumeInspection.shouldResume === false) {
1494
1521
  log('INFO', `[ModelPin] session ${session.id.slice(0, 8)} flagged as ${resumeInspection.reason}; starting fresh Claude session`);
1495
- session = createSession(sessionChatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', runtime.name);
1522
+ session = createSession(sessionChatId, effectiveCwd, boundProject && boundProject.name ? boundProject.name : '', runtime.name);
1496
1523
  } else if (resumeInspection.modelPin) {
1497
1524
  if (resumeInspection.modelPin !== model) {
1498
1525
  log('INFO', `[ModelPin] resuming ${session.id.slice(0, 8)} with original model ${resumeInspection.modelPin} (configured: ${model})`);
@@ -1546,6 +1573,8 @@ function createClaudeEngine(deps) {
1546
1573
  const memory = require('./memory');
1547
1574
 
1548
1575
  // L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
1576
+ // One-shot: inject once then clear, same pattern as compactContext.
1577
+ // Prevents re-injection on daemon restart or new session for the same chat.
1549
1578
  if (!session.started) {
1550
1579
  try {
1551
1580
  const nowDir = path.join(HOME, '.metame', 'memory', 'now');
@@ -1555,6 +1584,8 @@ function createClaudeEngine(deps) {
1555
1584
  const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
1556
1585
  if (nowContent) {
1557
1586
  memoryHint += `\n\n[Current task context:\n${nowContent}]`;
1587
+ // Clear after injection to prevent re-triggering on next session start
1588
+ try { fs.writeFileSync(nowPath, '', 'utf8'); } catch { /* non-critical */ }
1558
1589
  }
1559
1590
  }
1560
1591
  } catch { /* non-critical */ }
@@ -1605,6 +1636,33 @@ function createClaudeEngine(deps) {
1605
1636
  }
1606
1637
  }
1607
1638
 
1639
+ // Inject latest nightly insight (decisions/lessons) — one-liner per file, ~100 tokens
1640
+ if (!session.started) {
1641
+ try {
1642
+ const reflectDirs = [
1643
+ path.join(HOME, '.metame', 'memory', 'decisions'),
1644
+ path.join(HOME, '.metame', 'memory', 'lessons'),
1645
+ ];
1646
+ const reflectItems = [];
1647
+ for (const dir of reflectDirs) {
1648
+ if (!fs.existsSync(dir)) continue;
1649
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort();
1650
+ const latest = files[files.length - 1];
1651
+ if (!latest) continue;
1652
+ const content = fs.readFileSync(path.join(dir, latest), 'utf8');
1653
+ // Extract ## headings as one-line summaries (skip frontmatter)
1654
+ const headings = content.match(/^## .+$/gm);
1655
+ if (headings && headings.length > 0) {
1656
+ const type = dir.endsWith('decisions') ? 'decision' : 'lesson';
1657
+ reflectItems.push(...headings.slice(0, 2).map(h => `- [${type}] ${h.replace(/^## /, '')}`));
1658
+ }
1659
+ }
1660
+ if (reflectItems.length > 0) {
1661
+ memoryHint += `\n\n[Recent insights:\n${reflectItems.join('\n')}]`;
1662
+ }
1663
+ } catch { /* non-critical */ }
1664
+ }
1665
+
1608
1666
  memory.close();
1609
1667
  } catch (e) {
1610
1668
  if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
@@ -1621,10 +1679,16 @@ function createClaudeEngine(deps) {
1621
1679
  brainDoc = brain;
1622
1680
  const cmap = brain && brain.user_competence_map;
1623
1681
  if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
1624
- const lines = Object.entries(cmap)
1625
- .map(([domain, level]) => ` ${domain}: ${level}`)
1626
- .join('\n');
1627
- zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
1682
+ const entries = Object.entries(cmap);
1683
+ const allExpert = entries.every(([, level]) => String(level).toLowerCase() === 'expert');
1684
+ if (allExpert) {
1685
+ zdpHint = `\n- User is expert-level across all domains. Skip basics, no analogies needed.`;
1686
+ } else {
1687
+ const lines = entries
1688
+ .map(([domain, level]) => ` ${domain}: ${level}`)
1689
+ .join('\n');
1690
+ zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
1691
+ }
1628
1692
  }
1629
1693
  }
1630
1694
  } catch { /* non-critical */ }
@@ -1636,6 +1700,19 @@ function createClaudeEngine(deps) {
1636
1700
  } catch { /* ignore */ }
1637
1701
  }
1638
1702
 
1703
+ // Self-reflection patterns: behavioral guardrails distilled from past mistakes
1704
+ let reflectHint = '';
1705
+ if (!session.started && brainDoc) {
1706
+ try {
1707
+ const patterns = (brainDoc.growth && Array.isArray(brainDoc.growth.self_reflection_patterns))
1708
+ ? brainDoc.growth.self_reflection_patterns.filter(p => p && p.summary).slice(0, 3)
1709
+ : [];
1710
+ if (patterns.length > 0) {
1711
+ reflectHint = `\n- Self-correction patterns (avoid repeating these mistakes):\n${patterns.map(p => ` - ${String(p.summary).slice(0, 150)}`).join('\n')}`;
1712
+ }
1713
+ } catch { /* non-critical */ }
1714
+ }
1715
+
1639
1716
  // Inject daemon hints only on first message of a session
1640
1717
  // Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
1641
1718
  let daemonHint = '';
@@ -1654,7 +1731,7 @@ ${mentorRadarHint}
1654
1731
  Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
1655
1732
  daemonHint = `\n\n[System hints - DO NOT mention these to user:
1656
1733
  1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
1657
- 2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${taskRules}]`;
1734
+ 2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${reflectHint}${taskRules}]`;
1658
1735
  }
1659
1736
 
1660
1737
  daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
@@ -1686,7 +1763,7 @@ ${mentorRadarHint}
1686
1763
  if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
1687
1764
  summaryHint = `
1688
1765
 
1689
- [上次对话摘要,供参考]: ${_sess.last_summary}`;
1766
+ [上次对话摘要(历史已完成,仅供上下文,请勿重复执行)]: ${_sess.last_summary}`;
1690
1767
  log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
1691
1768
  }
1692
1769
  }
@@ -1751,20 +1828,19 @@ ${mentorRadarHint}
1751
1828
  const langGuard = session.started
1752
1829
  ? ''
1753
1830
  : '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
1831
+ // Intent hints are dynamic (per-prompt, semantic match), so compute for all runtimes.
1754
1832
  let intentHint = '';
1755
- if (runtime.name === 'codex') {
1756
- try {
1757
- const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
1758
- if (block) intentHint = `\n\n${block}`;
1759
- } catch (e) {
1760
- log('WARN', `Intent registry injection failed: ${e.message}`);
1761
- }
1833
+ try {
1834
+ const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
1835
+ if (block) intentHint = `\n\n${block}`;
1836
+ } catch (e) {
1837
+ log('WARN', `Intent registry injection failed: ${e.message}`);
1762
1838
  }
1763
- // For warm process reuse: context is already in the persistent process,
1764
- // so only send the user's actual prompt — skip all hint injection.
1765
- // This saves ~500-1500 tokens per turn and avoids context duplication.
1839
+ // For warm process reuse: static context (daemonHint, memoryHint, etc.) is already
1840
+ // in the persistent process — skip those to save tokens. intentHint is dynamic
1841
+ // (varies per prompt), so include it even on warm reuse.
1766
1842
  const fullPrompt = _warmEntry
1767
- ? routedPrompt
1843
+ ? routedPrompt + intentHint
1768
1844
  : routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
1769
1845
  if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
1770
1846
  const actualPermissionProfile = getActualCodexPermissionProfile(session);
@@ -1901,17 +1977,24 @@ ${mentorRadarHint}
1901
1977
  ...(runtime.name === 'codex' ? { runtimeSessionObserved: true } : {}),
1902
1978
  ...(runtime.name === 'codex' ? actualPermissionProfile : {}),
1903
1979
  };
1904
- return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
1980
+ return { ...cur, cwd: effectiveCwd || cur.cwd || HOME, engines };
1905
1981
  });
1906
1982
  if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
1907
1983
  log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
1908
1984
  }
1985
+ // Keep card header in sync with the real session ID reported by the engine
1986
+ if (_ackCardHeader && _ackCardHeader._baseTitle) {
1987
+ _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${safeNextId.slice(0, 8)})` };
1988
+ }
1909
1989
  };
1910
1990
 
1911
1991
  // Check if user cancelled during pre-spawn phase (sentinel was marked aborted)
1912
1992
  // Stamp session ID on card header so user can track session continuity
1993
+ if (_ackCardHeader) {
1994
+ _ackCardHeader._baseTitle = _ackCardHeader.title; // preserve original title for onSession updates
1995
+ }
1913
1996
  if (session && session.id && _ackCardHeader) {
1914
- _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader.title}(${session.id.slice(0, 8)})` };
1997
+ _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${session.id.slice(0, 8)})` };
1915
1998
  }
1916
1999
 
1917
2000
  const _preSentinel = activeProcesses.get(chatId);
@@ -1978,7 +2061,7 @@ ${mentorRadarHint}
1978
2061
  );
1979
2062
  session = createSession(
1980
2063
  sessionChatId,
1981
- session.cwd,
2064
+ effectiveCwd,
1982
2065
  boundProject && boundProject.name ? boundProject.name : '',
1983
2066
  'codex',
1984
2067
  requestedCodexPermissionProfile
@@ -2055,7 +2138,7 @@ ${mentorRadarHint}
2055
2138
  if (resumeFailure.kind !== 'interrupted') {
2056
2139
  session = createSession(
2057
2140
  sessionChatId,
2058
- session.cwd,
2141
+ effectiveCwd,
2059
2142
  boundProject && boundProject.name ? boundProject.name : '',
2060
2143
  'codex',
2061
2144
  requestedCodexPermissionProfile
@@ -2163,6 +2246,16 @@ ${mentorRadarHint}
2163
2246
  return { ok: true };
2164
2247
  }
2165
2248
 
2249
+ // Merge-pause with partial output: save card for reuse, discard partial output
2250
+ if (output && errorCode === 'INTERRUPTED_MERGE_PAUSE') {
2251
+ if (statusMsgId) {
2252
+ _pausedCards.set(chatId, { statusMsgId, cardHeader: _ackCardHeader, savedAt: Date.now() });
2253
+ if (bot.editMessage) bot.editMessage(chatId, statusMsgId, '⏸ 合并中…', _ackCardHeader).catch(() => {});
2254
+ log('INFO', `[askClaude] Merge-pause with partial output, saved card ${statusMsgId} for ${chatId}`);
2255
+ }
2256
+ return { ok: false, error: 'Paused for merge', errorCode };
2257
+ }
2258
+
2166
2259
  if (output) {
2167
2260
  if (runtime.name === 'codex') {
2168
2261
  _codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'interrupted'));
@@ -2338,6 +2431,12 @@ ${mentorRadarHint}
2338
2431
  } catch { /* non-critical — memory module may not be available */ }
2339
2432
  });
2340
2433
  }
2434
+ // Speculatively save card for pipeline post-resume flush reuse.
2435
+ // If no follow-up arrives, the card expires (30s TTL in _pausedCards consumer).
2436
+ const _replyMsgId = replyMsg && replyMsg.message_id;
2437
+ if (_replyMsgId && _ackCardHeader) {
2438
+ _pausedCards.set(chatId, { statusMsgId: _replyMsgId, cardHeader: _ackCardHeader, savedAt: Date.now() });
2439
+ }
2341
2440
  return { ok: !timedOut };
2342
2441
  } else {
2343
2442
  const errMsg = error || 'Unknown error';
@@ -2363,7 +2462,7 @@ ${mentorRadarHint}
2363
2462
  if (runtime.name === 'claude' && _isSessionResumeFail) {
2364
2463
  const _reason = errMsg.includes('already in use') ? 'locked' : _isThinkingSignatureError ? 'thinking-signature-invalid' : 'not found';
2365
2464
  log('WARN', `Session ${session.id} unusable (${_reason}), creating new`);
2366
- session = createSession(sessionChatId, session.cwd, '', runtime.name);
2465
+ session = createSession(sessionChatId, effectiveCwd, '', runtime.name);
2367
2466
 
2368
2467
  const retryArgs = runtime.buildArgs({
2369
2468
  model,
@@ -643,6 +643,19 @@ function createCommandRouter(deps) {
643
643
  return;
644
644
  }
645
645
 
646
+ // /btw — quick side question (read-only, concise, bypasses cooldown)
647
+ if (/^\/btw(\s|$)/i.test(text)) {
648
+ const btwQuestion = text.replace(/^\/btw\s*/i, '').trim();
649
+ if (!btwQuestion) {
650
+ await bot.sendMessage(chatId, '用法: /btw <问题>\n快速提问,不影响主会话节奏');
651
+ return;
652
+ }
653
+ const btwPrompt = `[Side question — answer concisely from existing context, no need for tools]\n\n${btwQuestion}`;
654
+ resetCooldown(chatId);
655
+ await askClaude(bot, chatId, btwPrompt, config, true, senderId);
656
+ return;
657
+ }
658
+
646
659
  if (text.startsWith('/')) {
647
660
  const currentModel = (config.daemon && config.daemon.model) || 'opus';
648
661
  const currentProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
@@ -664,6 +677,9 @@ function createCommandRouter(deps) {
664
677
  '/agent reset — 重置当前 Agent 角色',
665
678
  '/agent soul [repair] — 查看/修复 Agent Soul 身份层',
666
679
  '',
680
+ '💬 快捷:',
681
+ '/btw <问题> — 快速旁白提问(只读,不打断主任务)',
682
+ '',
667
683
  '📂 Session 管理:',
668
684
  '/new [path] [name] — 新建会话',
669
685
  '/sessions — 浏览所有最近会话',
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+
3
5
  function createCommandSessionResolver(deps) {
4
6
  const {
5
7
  path,
@@ -11,7 +13,7 @@ function createCommandSessionResolver(deps) {
11
13
  } = deps;
12
14
 
13
15
  function normalizeEngineName(name) {
14
- return String(name || '').trim().toLowerCase() === 'codex' ? 'codex' : getDefaultEngine();
16
+ return _normalizeEngine(name, getDefaultEngine);
15
17
  }
16
18
 
17
19
  function inferStoredEngine(rawSession) {
@@ -45,6 +45,7 @@ heartbeat:
45
45
  type: script
46
46
  command: node ~/.metame/distill.js
47
47
  interval: 4h
48
+ timeout: 5m
48
49
  precondition: "test -s ~/.metame/raw_signals.jsonl"
49
50
  require_idle: true
50
51
  notify: false
@@ -74,6 +75,7 @@ heartbeat:
74
75
  type: script
75
76
  command: node ~/.metame/skill-evolution.js
76
77
  interval: 12h
78
+ timeout: 5m
77
79
  precondition: "test -s ~/.metame/skill_signals.jsonl"
78
80
  require_idle: true
79
81
  notify: false
@@ -104,7 +106,7 @@ heartbeat:
104
106
  at: "01:30"
105
107
  require_idle: true
106
108
  notify: false
107
- enabled: true
109
+ enabled: false
108
110
 
109
111
  # Legacy flat tasks (no project isolation). New tasks should go under projects: above.
110
112
  # Examples — uncomment or add your own:
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync } = require('child_process');
7
+ const { normalizeEngineName } = require('./daemon-utils');
7
8
 
8
9
  const CODEX_TOOL_MAP = Object.freeze({
9
10
  command_execution: 'Bash',
@@ -14,11 +15,6 @@ const CODEX_TOOL_MAP = Object.freeze({
14
15
  web_fetch: 'WebFetch',
15
16
  });
16
17
 
17
- function normalizeEngineName(name) {
18
- const text = String(name || '').trim().toLowerCase();
19
- return text === 'codex' ? 'codex' : 'claude';
20
- }
21
-
22
18
  function resolveBinary(engineName, deps = {}) {
23
19
  const engine = normalizeEngineName(engineName);
24
20
  const home = deps.HOME || os.homedir();