metame-cli 1.5.16 → 1.5.18

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.
package/README.md CHANGED
@@ -79,21 +79,16 @@ Start on your laptop, continue on the train. `/stop` to interrupt, `/undo` to ro
79
79
 
80
80
  ### 3. Layered Memory That Works While You Sleep
81
81
 
82
- MetaMe's memory system runs automatically in the background — no prompts, no manual saves. Five layers, fully autonomous.
82
+ MetaMe's memory system runs automatically in the background — no prompts, no manual saves. Three layers, fully autonomous.
83
83
 
84
84
  **Layer 1 — Long-term Facts**
85
85
  When you go idle, MetaMe runs memory consolidation: extracts key decisions, patterns, and knowledge from your sessions into a persistent facts store. Facts can also carry concept labels (`fact_labels`) for faster cross-domain retrieval.
86
86
 
87
- **Layer 2 — Session Continuity**
88
- Resuming a conversation after 2+ hours? MetaMe injects a brief summary of what you were working on last time so you pick up where you left off without re-explaining context.
87
+ **Layer 2 — History Retrieval**
88
+ When you refer to "that thing we worked on last week", MetaMe uses session routing, topic tags, and memory recall to find the right thread, files, and facts. Same-session continuation stays native no synthetic resume summary is injected.
89
89
 
90
- **Layer 3 — Session Index**
91
- Every session gets tagged with topics and intent. This powers future session routing: when you reference "that thing we worked on last week", MetaMe knows where to look.
92
-
93
- **Layer 4 — Nightly Reflection**
90
+ **Layer 3 — Higher-order Memory**
94
91
  Every night at 01:00, MetaMe reviews your most-accessed facts from the past week and distills them into high-level decision logs and operational lessons. Distilled outputs are also written back to `memory.db` as `synthesized_insight`, enabling retrieval in future sessions.
95
-
96
- **Layer 5 — Memory Index**
97
92
  At 01:30, an auto-generated global index (`INDEX.md`) maps every memory document across all categories (including capsules and postmortems). This serves as a fast lookup table so MetaMe always knows where to find relevant context.
98
93
 
99
94
  ```
@@ -101,14 +96,13 @@ At 01:30, an auto-generated global index (`INDEX.md`) maps every memory document
101
96
  idle 30min → memory consolidation triggered
102
97
  → session_tags.json updated (topics indexed)
103
98
  → facts extracted → ~/.metame/memory.db
104
- → session summary cached → daemon_state.json
105
99
  01:00 → nightly reflection: hot facts → decisions + lessons
106
100
  01:30 → memory index regenerated
107
101
 
108
102
  [Next morning, when you resume]
109
103
  "continue from yesterday" →
110
- [上次对话摘要] Auth refactor, decided on JWT with
111
- refresh token rotation. Token expiry set to 15min.
104
+ session lookup + memory recall point to the right thread/files
105
+ without auto-injecting a synthetic conversation summary.
112
106
  ```
113
107
 
114
108
  ### 4. Heartbeat — A Programmable Nervous System
@@ -121,7 +115,7 @@ The heartbeat system is three-layered:
121
115
  Built into the daemon. Runs every 60 seconds regardless of what's in your config:
122
116
  - Drains the dispatch queue (IPC messages from other agents)
123
117
  - Tracks daemon aliveness and rotates logs
124
- - Detects when you go idle generates session continuity summaries
118
+ - Detects when you go idle and enters sleep mode for gated background work
125
119
 
126
120
  **Layer 1 — System Evolution (built-in defaults)**
127
121
  Five tasks shipped out of the box. They are precondition-gated and run only when useful:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.16",
3
+ "version": "1.5.18",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -6,7 +6,7 @@ const {
6
6
  USAGE_CATEGORY_LABEL,
7
7
  } = require('./usage-classifier');
8
8
  const { IS_WIN } = require('./platform');
9
- const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
9
+ const { ENGINE_MODEL_CONFIG, resolveEngineModel, normalizeClaudeModel } = require('./daemon-engine-runtime');
10
10
  const { resolveProjectKey: _resolveProjectKey } = require('./daemon-team-dispatch');
11
11
  const {
12
12
  parseRemoteTargetRef,
@@ -1439,7 +1439,7 @@ function createAdminCommandHandler(deps) {
1439
1439
  return { handled: true, config };
1440
1440
  }
1441
1441
 
1442
- // /doctor — diagnostics; /fix — restore backup; /reset — reset model to sonnet
1442
+ // /doctor — diagnostics; /fix — restore backup; /reset — reset Claude slot to opus
1443
1443
  if (text === '/fix') {
1444
1444
  if (restoreConfig()) {
1445
1445
  await bot.sendMessage(chatId, '✅ 已从备份恢复配置');
@@ -1453,7 +1453,8 @@ function createAdminCommandHandler(deps) {
1453
1453
  backupConfig();
1454
1454
  const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
1455
1455
  if (!cfg.daemon) cfg.daemon = {};
1456
- cfg.daemon.model = 'opus';
1456
+ if (!cfg.daemon.models) cfg.daemon.models = {};
1457
+ cfg.daemon.models.claude = 'opus';
1457
1458
  writeConfigSafe(cfg);
1458
1459
  config = loadConfig();
1459
1460
  await bot.sendMessage(chatId, '✅ 模型已重置为 opus');
@@ -1480,9 +1481,10 @@ function createAdminCommandHandler(deps) {
1480
1481
  issues++;
1481
1482
  }
1482
1483
 
1483
- const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
1484
+ const daemonCfg = (cfg && cfg.daemon) || {};
1485
+ const m = resolveEngineModel('claude', daemonCfg);
1484
1486
  const modelOk = isCustomProvider
1485
- ? /^[a-zA-Z0-9._-]{2,80}$/.test(String(m || '').trim())
1487
+ ? ['sonnet', 'opus', 'haiku'].includes(m)
1486
1488
  : validModels.includes(m);
1487
1489
  if (modelOk) {
1488
1490
  checks.push(`✅ 模型: ${m}`);
@@ -1569,7 +1571,11 @@ function createAdminCommandHandler(deps) {
1569
1571
  : engineCfg.provider;
1570
1572
  const isBuiltinProvider = activeProvider === engineCfg.provider;
1571
1573
  const distillModel = getDistillModel();
1572
- const hintLine = engineCfg.hint ? `\n💡 ${engineCfg.hint}` : (!isBuiltinProvider ? `\n💡 ${activeProvider} 可输入任意模型名` : '');
1574
+ const hintLine = engineCfg.hint
1575
+ ? `\n💡 ${engineCfg.hint}`
1576
+ : (!isBuiltinProvider && currentEngine === 'claude'
1577
+ ? `\n💡 ${activeProvider} 的后端真实模型请在 CC Switch / provider 层配置`
1578
+ : '');
1573
1579
 
1574
1580
  if (!arg) {
1575
1581
  const statusLine = `🤖 [${currentEngine}] 会话模型: ${currentModel} Provider: ${activeProvider}\n🧪 后台轻量: ${distillModel} (/distill-model 修改)${hintLine}`;
@@ -1587,10 +1593,13 @@ function createAdminCommandHandler(deps) {
1587
1593
  }
1588
1594
 
1589
1595
  const normalizedArg = arg.toLowerCase();
1590
- // Only restrict model names for claude with anthropic provider (known fixed set)
1591
- // codex always allows free-form input (OpenAI models change frequently)
1592
- if (currentEngine === 'claude' && isBuiltinProvider && !optionValues.includes(normalizedArg)) {
1593
- await bot.sendMessage(chatId, `❌ 无效模型: ${arg}\n可选: ${optionValues.join(', ')}\n💡 切换到自定义 provider 后可用任意模型名`);
1596
+ // Claude session/config layer only accepts canonical slots; provider mapping stays in CC Switch.
1597
+ if (currentEngine === 'claude' && !optionValues.includes(normalizedArg)) {
1598
+ const suggested = normalizeClaudeModel(arg, '');
1599
+ const hint = suggested
1600
+ ? `\n💡 检测到它更像 Claude 槽位 ${suggested},请直接用 /model ${suggested}`
1601
+ : '';
1602
+ await bot.sendMessage(chatId, `❌ Claude 会话模型只接受: ${optionValues.join(', ')}\n后端真实模型请在 CC Switch / provider 层配置,不要写进会话模型${hint}`);
1594
1603
  return { handled: true, config };
1595
1604
  }
1596
1605
 
@@ -434,13 +434,10 @@ function createClaudeEngine(deps) {
434
434
  const entry = JSON.parse(line);
435
435
  const sessionModel = entry && entry.message && entry.message.model;
436
436
  if (!sessionModel || sessionModel === '<synthetic>') continue;
437
- if (!sessionModel.startsWith('claude-')) {
438
- return {
439
- shouldResume: false,
440
- modelPin: null,
441
- reason: 'non-claude-session',
442
- };
443
- }
437
+ // Custom Claude-compatible providers may record backend-native model ids
438
+ // (for example MiniMax) in the JSONL. Those sessions are still resumable;
439
+ // we only skip model pinning when the family cannot be mapped back to
440
+ // Claude's alias set.
444
441
  // If the configured model is a short alias (sonnet/opus/haiku) and the JSONL model
445
442
  // belongs to the same family, do NOT pin — let the alias resolve to the latest version.
446
443
  // Only pin when the families genuinely differ (e.g. session was opus, config says sonnet).
@@ -1235,14 +1232,18 @@ function createClaudeEngine(deps) {
1235
1232
  * Reset active provider back to anthropic/opus and reload config.
1236
1233
  * Returns the freshly loaded config so callers can reassign their local variable.
1237
1234
  */
1238
- function fallbackToDefaultProvider(reason) {
1235
+ function fallbackToDefaultProvider(reason, boundProjectKey = '') {
1239
1236
  log('WARN', `Falling back to anthropic/opus — reason: ${reason}`);
1240
1237
  if (providerMod && providerMod.getActiveName() !== 'anthropic') {
1241
1238
  providerMod.setActive('anthropic');
1242
1239
  }
1243
1240
  const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
1244
1241
  if (!cfg.daemon) cfg.daemon = {};
1245
- cfg.daemon.model = 'opus';
1242
+ if (!cfg.daemon.models) cfg.daemon.models = {};
1243
+ cfg.daemon.models.claude = 'opus';
1244
+ if (boundProjectKey && cfg.projects && cfg.projects[boundProjectKey]) {
1245
+ cfg.projects[boundProjectKey].model = 'opus';
1246
+ }
1246
1247
  writeConfigSafe(cfg);
1247
1248
  return loadConfig();
1248
1249
  }
@@ -1751,25 +1752,6 @@ ${mentorRadarHint}
1751
1752
  6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
1752
1753
  }
1753
1754
 
1754
- // P2-B: inject session summary when resuming after a 2h+ gap
1755
- let summaryHint = '';
1756
- if (session.started) {
1757
- try {
1758
- const _stSum = loadState();
1759
- const _sess = _stSum.sessions && _stSum.sessions[chatId];
1760
- if (_sess && _sess.last_summary && _sess.last_summary_at) {
1761
- const _idleMs = Date.now() - (_sess.last_active || 0);
1762
- const _summaryAgeH = (Date.now() - _sess.last_summary_at) / 3600000;
1763
- if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
1764
- summaryHint = `
1765
-
1766
- [上次对话摘要(历史已完成,仅供上下文,请勿重复执行)]: ${_sess.last_summary}`;
1767
- log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
1768
- }
1769
- }
1770
- } catch { /* non-critical */ }
1771
- }
1772
-
1773
1755
  // Mentor context hook: inject after memoryHint, before langGuard.
1774
1756
  let mentorHint = '';
1775
1757
  if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
@@ -1841,7 +1823,7 @@ ${mentorRadarHint}
1841
1823
  // (varies per prompt), so include it even on warm reuse.
1842
1824
  const fullPrompt = _warmEntry
1843
1825
  ? routedPrompt + intentHint
1844
- : routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
1826
+ : routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + memoryHint + mentorHint + langGuard;
1845
1827
  if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
1846
1828
  const actualPermissionProfile = getActualCodexPermissionProfile(session);
1847
1829
  if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
@@ -2269,7 +2251,7 @@ ${mentorRadarHint}
2269
2251
  const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
2270
2252
  if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
2271
2253
  try {
2272
- config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
2254
+ config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`, boundProjectKey || '');
2273
2255
  await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
2274
2256
  } catch (fbErr) {
2275
2257
  log('ERROR', `Fallback failed: ${fbErr.message}`);
@@ -2500,7 +2482,7 @@ ${mentorRadarHint}
2500
2482
  const builtinModelValues = (ENGINE_MODEL_CONFIG.claude.options || []).map(o => typeof o === 'string' ? o : o.value);
2501
2483
  if ((activeProv !== 'anthropic' || !builtinModelValues.includes(model)) && !errMsg.includes('Stopped by user')) {
2502
2484
  try {
2503
- config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
2485
+ config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`, boundProjectKey || '');
2504
2486
  await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
2505
2487
  } catch (fallbackErr) {
2506
2488
  log('ERROR', `Fallback failed: ${fallbackErr.message}`);
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { resolveEngineModel } = require('./daemon-engine-runtime');
4
+
3
5
  function createCommandRouter(deps) {
4
6
  const {
5
7
  loadState,
@@ -657,7 +659,7 @@ function createCommandRouter(deps) {
657
659
  }
658
660
 
659
661
  if (text.startsWith('/')) {
660
- const currentModel = (config.daemon && config.daemon.model) || 'opus';
662
+ const currentModel = resolveEngineModel('claude', (config && config.daemon) || {});
661
663
  const currentProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
662
664
  await bot.sendMessage(chatId, [
663
665
  '📱 手机端 Claude Code',
@@ -93,23 +93,58 @@ const BUILTIN_CLAUDE_MODEL_VALUES = Object.freeze(
93
93
  ).filter(Boolean)
94
94
  );
95
95
 
96
+ function normalizeClaudeModel(model, fallback = ENGINE_MODEL_CONFIG.claude.main) {
97
+ const raw = String(model || '').trim();
98
+ if (!raw) return fallback;
99
+ const normalized = raw.toLowerCase();
100
+ if (BUILTIN_CLAUDE_MODEL_VALUES.includes(normalized)) return normalized;
101
+ if (normalized.includes('opus')) return 'opus';
102
+ if (normalized.includes('sonnet')) return 'sonnet';
103
+ if (normalized.includes('haiku')) return 'haiku';
104
+ return fallback;
105
+ }
106
+
107
+ function looksLikeCodexModel(model) {
108
+ const raw = String(model || '').trim().toLowerCase();
109
+ if (!raw) return false;
110
+ return (
111
+ raw.startsWith('gpt-')
112
+ || raw.startsWith('o1')
113
+ || raw.startsWith('o3')
114
+ || raw.startsWith('o4')
115
+ || raw.includes('codex')
116
+ );
117
+ }
118
+
96
119
  function resolveEngineModel(engineName, daemonCfg = {}, overrideModel = '') {
97
120
  const engine = normalizeEngineName(engineName);
98
121
  const engineCfg = ENGINE_MODEL_CONFIG[engine] || ENGINE_MODEL_CONFIG.claude;
99
122
  const engineModels = (daemonCfg && daemonCfg.models) || {};
100
123
  const explicitModel = String(overrideModel || '').trim();
101
- if (explicitModel) return explicitModel;
124
+ if (explicitModel) {
125
+ return engine === 'claude'
126
+ ? normalizeClaudeModel(explicitModel, engineCfg.main)
127
+ : explicitModel;
128
+ }
102
129
 
103
130
  const perEngineModel = String(engineModels[engine] || '').trim();
104
- if (perEngineModel) return perEngineModel;
131
+ if (perEngineModel) {
132
+ return engine === 'claude'
133
+ ? normalizeClaudeModel(perEngineModel, engineCfg.main)
134
+ : perEngineModel;
135
+ }
105
136
 
106
137
  const legacyModel = String((daemonCfg && daemonCfg.model) || '').trim();
107
138
  if (!legacyModel) return engineCfg.main;
108
139
 
109
140
  // Legacy daemon.model historically meant a Claude model.
110
- // Preserve backward compatibility for non-Claude custom model IDs,
111
- // but do not leak Claude aliases like "opus" into Codex sessions.
112
- if (engine === 'codex' && BUILTIN_CLAUDE_MODEL_VALUES.includes(legacyModel)) {
141
+ if (engine === 'claude') {
142
+ return normalizeClaudeModel(legacyModel, engineCfg.main);
143
+ }
144
+
145
+ // Legacy daemon.model primarily belonged to Claude; only reuse it for Codex
146
+ // when it already looks like a real Codex/OpenAI model id.
147
+ if (engine === 'codex' && !looksLikeCodexModel(legacyModel)) {
113
148
  return engineCfg.main;
114
149
  }
115
150
  return legacyModel;
@@ -420,6 +455,7 @@ module.exports = {
420
455
  resolveBinary,
421
456
  detectDefaultEngine,
422
457
  resolveEngineModel,
458
+ normalizeClaudeModel,
423
459
  ENGINE_MODEL_CONFIG,
424
460
  ENGINE_DISTILL_MAP,
425
461
  ENGINE_DEFAULT_MODEL,
@@ -434,5 +470,7 @@ module.exports = {
434
470
  normalizeCodexApprovalPolicy,
435
471
  resolveCodexPermissionProfile,
436
472
  BUILTIN_CLAUDE_MODEL_VALUES,
473
+ normalizeClaudeModel,
474
+ looksLikeCodexModel,
437
475
  },
438
476
  };
@@ -296,7 +296,9 @@ function createSessionStore(deps) {
296
296
  }
297
297
  } catch {}
298
298
  }
299
- } catch { /* skip */ }
299
+ } catch (err) {
300
+ log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
301
+ }
300
302
 
301
303
  try {
302
304
  const files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
@@ -318,7 +320,9 @@ function createSessionStore(deps) {
318
320
  });
319
321
  }
320
322
  }
321
- } catch { /* skip */ }
323
+ } catch (err) {
324
+ log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
325
+ }
322
326
  }
323
327
 
324
328
  const all = Array.from(sessionMap.values()).map((entry) => ({ ...entry, engine: 'claude' }));
@@ -385,7 +389,8 @@ function createSessionStore(deps) {
385
389
  } catch { /* non-fatal */ }
386
390
  }
387
391
  return all;
388
- } catch {
392
+ } catch (err) {
393
+ log('WARN', `scanClaudeSessions: ${err.message}`);
389
394
  return [];
390
395
  }
391
396
  }
@@ -444,8 +449,9 @@ function createSessionStore(deps) {
444
449
  };
445
450
  })
446
451
  .map((session) => enrichCodexSession(session));
447
- } catch {
452
+ } catch (err) {
448
453
  if (db) { try { db.close(); } catch { /* ignore */ } }
454
+ log('WARN', `scanCodexSessions ${CODEX_DB}: ${err.message}`);
449
455
  return [];
450
456
  }
451
457
  }
@@ -544,19 +550,15 @@ function createSessionStore(deps) {
544
550
 
545
551
  function scanAllSessions() {
546
552
  if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
547
- try {
548
- const all = [...scanClaudeSessions(), ...scanCodexSessions()];
549
- all.sort((a, b) => {
550
- const aTime = a.fileMtime || new Date(a.modified).getTime();
551
- const bTime = b.fileMtime || new Date(b.modified).getTime();
552
- return bTime - aTime;
553
- });
554
- _sessionCache = all;
555
- _sessionCacheTime = Date.now();
556
- return all;
557
- } catch {
558
- return [];
559
- }
553
+ const all = [...scanClaudeSessions(), ...scanCodexSessions()];
554
+ all.sort((a, b) => {
555
+ const aTime = a.fileMtime || new Date(a.modified).getTime();
556
+ const bTime = b.fileMtime || new Date(b.modified).getTime();
557
+ return bTime - aTime;
558
+ });
559
+ _sessionCache = all;
560
+ _sessionCacheTime = Date.now();
561
+ return all;
560
562
  }
561
563
 
562
564
  function listRecentSessions(limit, cwd, engine) {
@@ -1033,10 +1035,6 @@ function createSessionStore(deps) {
1033
1035
 
1034
1036
  // Try to read cwd/model from session JSONL file content (most reliable)
1035
1037
  const metadata = _readClaudeSessionMetadata(sessionFile);
1036
- if (metadata.model && !metadata.model.startsWith('claude-')) {
1037
- log('WARN', `[SessionValid] ${sessionId.slice(0, 8)}: non-claude model "${metadata.model}"`);
1038
- return false;
1039
- }
1040
1038
  if (metadata.cwd && path.resolve(metadata.cwd) === normCwd) return true;
1041
1039
  if (metadata.cwd) {
1042
1040
  // CWD mismatch: the session was created for a different directory.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const { classifyTaskUsage } = require('./usage-classifier');
5
+ const { resolveEngineModel } = require('./daemon-engine-runtime');
5
6
 
6
7
  const WEEKDAY_INDEX = Object.freeze({
7
8
  sun: 0,
@@ -193,7 +194,6 @@ function createTaskScheduler(deps) {
193
194
  isUserIdle,
194
195
  isInSleepMode,
195
196
  setSleepMode,
196
- spawnSessionSummaries,
197
197
  getWakeRecoveryHook,
198
198
  skillEvolution,
199
199
  } = deps;
@@ -579,7 +579,7 @@ function createTaskScheduler(deps) {
579
579
  if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
580
580
 
581
581
  // Workflow tasks match the user's session model setting (same quality as interactive)
582
- const sessionModel = (config && config.daemon && config.daemon.model) || 'sonnet';
582
+ const sessionModel = resolveEngineModel('claude', (config && config.daemon) || {});
583
583
  const model = normalizeModel(task.model || sessionModel);
584
584
  const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
585
585
  const sessionId = crypto.randomUUID();
@@ -743,8 +743,6 @@ function createTaskScheduler(deps) {
743
743
  if (idle && !isInSleepMode()) {
744
744
  setSleepMode(true);
745
745
  log('INFO', '[DAEMON] Entering Sleep Mode');
746
- // Generate summaries for sessions idle 2-24h
747
- spawnSessionSummaries();
748
746
  } else if (!idle && isInSleepMode()) {
749
747
  setSleepMode(false);
750
748
  log('INFO', '[DAEMON] Exiting Sleep Mode — local activity detected');
package/scripts/daemon.js CHANGED
@@ -1180,67 +1180,6 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
1180
1180
  };
1181
1181
  }
1182
1182
 
1183
- /**
1184
- * Spawn memory-extract.js as a detached background process.
1185
- * Called on sleep mode entry to consolidate session facts.
1186
- */
1187
- /**
1188
- * Spawn session-summarize.js for sessions that have been idle 2-24 hours.
1189
- * Called on sleep mode entry. Skips sessions that already have a fresh summary.
1190
- */
1191
- const MAX_CONCURRENT_SUMMARIES = 3;
1192
-
1193
- function spawnSessionSummaries() {
1194
- const scriptPath = path.join(__dirname, 'session-summarize.js');
1195
- if (!fs.existsSync(scriptPath)) return;
1196
- const state = loadState();
1197
- const now = Date.now();
1198
- const TWO_HOURS = 2 * 60 * 60 * 1000;
1199
- const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
1200
- // Collect eligible sessions, sort by most recently active first
1201
- const eligible = [];
1202
- for (const [cid, sess] of Object.entries(state.sessions || {})) {
1203
- // Support both old flat format and new per-engine format
1204
- let sessionId, started;
1205
- if (sess.engines) {
1206
- const active = Object.values(sess.engines).find(s => s.id && s.started);
1207
- if (!active) continue;
1208
- sessionId = active.id;
1209
- started = true;
1210
- } else {
1211
- sessionId = sess.id;
1212
- started = sess.started;
1213
- }
1214
- if (!sessionId || !started) continue;
1215
- const lastActive = sess.last_active || 0;
1216
- const idleMs = now - lastActive;
1217
- if (idleMs < TWO_HOURS || idleMs > SEVEN_DAYS) continue;
1218
- if ((sess.last_summary_at || 0) > lastActive) continue;
1219
- eligible.push({ cid, sess: { ...sess, id: sessionId, started }, lastActive });
1220
- }
1221
- eligible.sort((a, b) => b.lastActive - a.lastActive);
1222
-
1223
- let spawned = 0;
1224
- for (const { cid, sess } of eligible) {
1225
- if (spawned >= MAX_CONCURRENT_SUMMARIES) {
1226
- log('INFO', `[DAEMON] Session summary concurrency limit (${MAX_CONCURRENT_SUMMARIES}) reached, deferring remaining`);
1227
- break;
1228
- }
1229
- const idleMs = now - (sess.last_active || 0);
1230
- try {
1231
- const child = spawn(process.execPath, [scriptPath, cid, sess.id], {
1232
- detached: true, stdio: 'ignore',
1233
- ...(process.platform === 'win32' ? { windowsHide: true } : {}),
1234
- });
1235
- child.unref();
1236
- spawned++;
1237
- log('INFO', `[DAEMON] Session summary spawned for ${cid} (idle ${Math.round(idleMs / 3600000)}h)`);
1238
- } catch (e) {
1239
- log('WARN', `[DAEMON] Failed to spawn session summary: ${e.message}`);
1240
- }
1241
- }
1242
- }
1243
-
1244
1183
  /**
1245
1184
  * Physiological heartbeat: zero-token awareness check.
1246
1185
  * Runs every tick unconditionally.
@@ -2078,16 +2017,6 @@ if (providerMod && typeof providerMod.setEngine === 'function') {
2078
2017
  }
2079
2018
  log('INFO', `Default engine: ${_defaultEngine} (detected: ${detectedEngine})`);
2080
2019
 
2081
- // One-time migration: daemon.model (legacy) → daemon.models.<engine>
2082
- try {
2083
- const _migCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
2084
- if (_migCfg.daemon && _migCfg.daemon.model && !_migCfg.daemon.models) {
2085
- _migCfg.daemon.models = { [_defaultEngine]: _migCfg.daemon.model };
2086
- writeConfigSafe(_migCfg);
2087
- log('INFO', `Migrated daemon.model="${_migCfg.daemon.model}" → daemon.models.${_defaultEngine}`);
2088
- }
2089
- } catch { /* ignore */ }
2090
-
2091
2020
  function getDefaultEngine() {
2092
2021
  return _defaultEngine;
2093
2022
  }
@@ -2107,15 +2036,6 @@ function setDefaultEngine(engine) {
2107
2036
  try { providerMod.setEngine(engine); } catch { /* ignore */ }
2108
2037
  }
2109
2038
  }
2110
- // Migrate old daemon.model → daemon.models[engine] on first switch
2111
- try {
2112
- const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
2113
- if (!cfg.daemon) cfg.daemon = {};
2114
- if (cfg.daemon.model && !cfg.daemon.models) {
2115
- cfg.daemon.models = { [engine]: cfg.daemon.model };
2116
- writeConfigSafe(cfg);
2117
- }
2118
- } catch { /* ignore */ }
2119
2039
  }
2120
2040
 
2121
2041
  const getEngineRuntime = createEngineRuntimeFactory({
@@ -2156,7 +2076,6 @@ const {
2156
2076
  isUserIdle,
2157
2077
  isInSleepMode: () => _inSleepMode,
2158
2078
  setSleepMode: (next) => { _inSleepMode = !!next; },
2159
- spawnSessionSummaries,
2160
2079
  getWakeRecoveryHook: () => wakeRecoveryHook,
2161
2080
  skillEvolution,
2162
2081
  });
@@ -2609,10 +2528,6 @@ async function main() {
2609
2528
  'enable_nl_mac_control',
2610
2529
  'enable_nl_mac_fallback',
2611
2530
  ];
2612
- // All known models across all engines (for legacy daemon.model validation only)
2613
- const BUILTIN_CLAUDE_MODELS = (ENGINE_MODEL_CONFIG.claude.options || []).map(option =>
2614
- typeof option === 'string' ? option : option.value
2615
- ).filter(Boolean);
2616
2531
  for (const key of Object.keys(config)) {
2617
2532
  if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
2618
2533
  }
@@ -2620,14 +2535,11 @@ async function main() {
2620
2535
  for (const key of Object.keys(config.daemon)) {
2621
2536
  if (!KNOWN_DAEMON.includes(key)) log('WARN', `Config: unknown daemon.${key} (typo?)`);
2622
2537
  }
2623
- // Validate legacy daemon.model (only warn if anthropic provider + unknown Claude model)
2624
- if (config.daemon.model && !BUILTIN_CLAUDE_MODELS.includes(config.daemon.model)) {
2538
+ // Keep legacy daemon.model read-compatible, but never auto-migrate or treat it
2539
+ // as the write source of truth. The canonical writable field is daemon.models.<engine>.
2540
+ if (config.daemon.model) {
2625
2541
  const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
2626
- if (activeProv === 'anthropic' && _defaultEngine === 'claude') {
2627
- log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known Claude model`);
2628
- } else {
2629
- log('INFO', `Config: legacy daemon.model="${config.daemon.model}" retained; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
2630
- }
2542
+ log('INFO', `Config: legacy daemon.model detected; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
2631
2543
  }
2632
2544
  }
2633
2545
 
@@ -46,6 +46,8 @@ feishu:
46
46
  - 引擎 runtime 工厂:`scripts/daemon-engine-runtime.js`
47
47
  - 会话执行入口:`scripts/daemon-claude-engine.js`(Claude/Codex 共用)
48
48
  - Session 回写:`patchSessionSerialized()` 串行化,避免 thread.started 竞态覆盖
49
+ - 同一个底层 session 不做自动“恢复摘要”注入;续聊直接依赖引擎原生上下文
50
+ - 额外上下文只在显式链路进入时注入,例如 `/compact` 产物、`NOW.md`、memory facts / capsules、intent hints
49
51
 
50
52
  ### Codex 会话策略
51
53
 
@@ -95,6 +97,10 @@ feishu:
95
97
  - Dispatch 签名密钥:`~/.metame/.dispatch_secret`(自动创建)
96
98
  - 自动更新策略:发布版 npm 安装默认开启;源码 checkout / `npm link` 默认关闭,可用 `METAME_AUTO_UPDATE=on|off` 覆盖
97
99
 
100
+ 说明:
101
+ - `daemon_state.json` 仍保存 session 元数据(如 `last_active`),用于路由、恢复和状态判断。
102
+ - 不再缓存或注入“闲置后恢复摘要”;如果需要压缩上下文,只走显式 `/compact`。
103
+
98
104
  ## 7. 热重载安全机制(三层防护)
99
105
 
100
106
  1. **部署前预检**(`index.js`):`node -c` 语法检查所有 `.js`,不通过则拒绝以 copy 模式部署到 `~/.metame/`
@@ -11,7 +11,7 @@
11
11
 
12
12
  | 文件 | 引用方 | 说明 |
13
13
  |------|--------|------|
14
- | `session-analytics.js` | daemon-claude-engine, distill, memory-extract, session-summarize | 会话分析核心库 |
14
+ | `session-analytics.js` | daemon-claude-engine, distill, memory-extract | 会话分析核心库 |
15
15
  | `mentor-engine.js` | daemon-claude-engine, daemon-admin-commands | AI 导师引擎 |
16
16
  | `intent-registry.js` | daemon-claude-engine, hooks/intent-engine | 意图识别注册表 |
17
17
  | `daemon-command-session-route.js` | daemon-exec-commands, daemon-ops-commands | 会话路由解析 |
@@ -19,7 +19,6 @@
19
19
  | `daemon-siri-imessage.js` | daemon-siri-bridge.js | iMessage 数据库读取 |
20
20
  | `telegram-adapter.js` | daemon-bridges.js | Telegram 适配器 |
21
21
  | `feishu-adapter.js` | daemon-bridges.js | 飞书适配器 |
22
- | `session-summarize.js` | daemon.js (spawn) | 会话总结,由 daemon.js 第1158行 spawn 调用 |
23
22
 
24
23
  ### HEARTBEAT — daemon.yaml 心跳任务调用(无需处理)
25
24
 
@@ -28,6 +28,7 @@
28
28
  - 会话与引擎选择:
29
29
  - `scripts/daemon-claude-engine.js`
30
30
  - 关键点:`askClaude()` 按 `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
31
+ - 说明:同一底层 session 续聊不再注入会话恢复摘要;额外上下文仅来自显式 compact / memory / intent 链路
31
32
  - Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
32
33
 
33
34
  - Agent Soul 身份层(新):
@@ -1,118 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * session-summarize.js <chatId> <sessionId>
4
- * Generates a 3-5 sentence summary for an idle session via Haiku,
5
- * stores it in daemon_state.json for injection on next resume.
6
- *
7
- * Uses session-analytics.extractSkeleton() for robust JSONL parsing
8
- * (handles tool_use, artifacts, empty chunks without crashing).
9
- */
10
- 'use strict';
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
-
16
- const [,, chatId, sessionId] = process.argv;
17
- if (!chatId || !sessionId) {
18
- console.error('Usage: session-summarize.js <chatId> <sessionId>');
19
- process.exit(1);
20
- }
21
-
22
- const HOME = os.homedir();
23
- const METAME_DIR = path.join(HOME, '.metame');
24
- const STATE_FILE = path.join(METAME_DIR, 'daemon_state.json');
25
- const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
26
-
27
- function findSessionFile(sid) {
28
- try {
29
- for (const dir of fs.readdirSync(CLAUDE_PROJECTS)) {
30
- const p = path.join(CLAUDE_PROJECTS, dir, `${sid}.jsonl`);
31
- if (fs.existsSync(p)) return p;
32
- }
33
- } catch { /* ignore */ }
34
- return null;
35
- }
36
-
37
- function loadState() {
38
- try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return {}; }
39
- }
40
-
41
- function saveState(state) {
42
- try { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8'); } catch { /* ignore */ }
43
- }
44
-
45
- async function main() {
46
- const sessionFile = findSessionFile(sessionId);
47
- if (!sessionFile) {
48
- console.log(`[session-summarize] Session file not found for ${sessionId.slice(0, 8)}`);
49
- return;
50
- }
51
-
52
- // Use extractSkeleton for robust parsing — already battle-tested on 100+ sessions.
53
- // Handles tool_use blocks, artifacts, empty chunks, malformed lines gracefully.
54
- let skeleton;
55
- try {
56
- const analytics = require('./session-analytics');
57
- skeleton = analytics.extractSkeleton(sessionFile);
58
- } catch (e) {
59
- console.log(`[session-summarize] extractSkeleton failed: ${e.message}`);
60
- return;
61
- }
62
-
63
- const snippets = skeleton.user_snippets || [];
64
- if (snippets.length < 2) {
65
- console.log(`[session-summarize] Too few user messages (${snippets.length}), skipping`);
66
- return;
67
- }
68
-
69
- let callHaiku;
70
- try {
71
- callHaiku = require('./providers').callHaiku;
72
- } catch (e) {
73
- console.log(`[session-summarize] providers not available: ${e.message}`);
74
- return;
75
- }
76
-
77
- // Build compact context from skeleton (safe strings, already sliced to 100 chars each)
78
- const snippetText = snippets.join('\n- ');
79
- const meta = [
80
- skeleton.project ? `项目: ${skeleton.project}` : '',
81
- skeleton.intent ? `首要意图: ${skeleton.intent}` : '',
82
- skeleton.duration_min ? `时长: ${skeleton.duration_min}分钟` : '',
83
- skeleton.total_tool_calls ? `工具调用: ${skeleton.total_tool_calls}次` : '',
84
- ].filter(Boolean).join(',');
85
-
86
- const prompt = `请用2-4句话简洁总结以下会话的核心内容和关键结论。只说结果和决策,不列举过程。中文输出。
87
-
88
- ${meta}
89
-
90
- 用户主要说了什么:
91
- - ${snippetText}`;
92
-
93
- let summary;
94
- try {
95
- summary = await Promise.race([
96
- callHaiku(prompt, {}, 30000),
97
- new Promise((_, r) => setTimeout(() => r(new Error('timeout')), 35000)),
98
- ]);
99
- summary = (summary || '').trim().slice(0, 500);
100
- } catch (e) {
101
- console.log(`[session-summarize] Haiku call failed: ${e.message}`);
102
- return;
103
- }
104
-
105
- if (!summary) return;
106
-
107
- const state = loadState();
108
- if (!state.sessions) state.sessions = {};
109
- if (!state.sessions[chatId]) state.sessions[chatId] = {};
110
- state.sessions[chatId].last_summary = summary;
111
- state.sessions[chatId].last_summary_at = Date.now();
112
- state.sessions[chatId].last_summary_session_id = sessionId;
113
- saveState(state);
114
-
115
- console.log(`[session-summarize] Saved for ${chatId} (${sessionId.slice(0, 8)}): ${summary.slice(0, 80)}...`);
116
- }
117
-
118
- main().catch(e => console.error(`[session-summarize] Fatal: ${e.message}`));