metame-cli 1.4.34 → 1.5.1

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 (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. package/scripts/utils.test.js +0 -192
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * MetaMe Passive Distiller
5
5
  *
6
- * Reads raw signal buffer, calls Claude (haiku, non-interactive)
6
+ * Reads raw signal buffer, calls Claude (configured distill model, non-interactive)
7
7
  * to extract persistent preferences/identity, merges into profile.
8
8
  *
9
9
  * Runs automatically before each MetaMe session launch.
@@ -15,11 +15,14 @@ const os = require('os');
15
15
  const { callHaiku, buildDistillEnv } = require('./providers');
16
16
 
17
17
  const HOME = os.homedir();
18
+ const METAME_DIR = path.join(HOME, '.metame');
18
19
  const BUFFER_FILE = path.join(HOME, '.metame', 'raw_signals.jsonl');
19
20
  const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
20
21
  const LOCK_FILE = path.join(HOME, '.metame', 'distill.lock');
22
+ const MEMORY_DIR = path.join(METAME_DIR, 'memory');
23
+ const POSTMORTEM_DIR = path.join(MEMORY_DIR, 'postmortems');
21
24
 
22
- const { hasKey, isLocked, getTier, getWritableKeysForPrompt, estimateTokens, TOKEN_BUDGET } = require('./schema');
25
+ const { hasKey, isLocked, getTier, getDefinition, getWritableKeysForPrompt, estimateTokens, TOKEN_BUDGET } = require('./schema');
23
26
  const { loadPending, savePending, upsertPending, getPromotable, removePromoted, expireStale } = require('./pending-traits');
24
27
  const { writeBrainFileSafe, normalizeProjectPath, deriveProjectInfo } = require('./utils');
25
28
 
@@ -37,6 +40,248 @@ try {
37
40
  distillEnv = buildDistillEnv();
38
41
  } catch { /* providers not configured — use defaults */ }
39
42
 
43
+ const COMPETENCE_SCORE = {
44
+ beginner: 1,
45
+ intermediate: 2,
46
+ expert: 3,
47
+ };
48
+
49
+ function normalizeCompetenceLevel(level) {
50
+ const v = String(level || '').trim().toLowerCase();
51
+ if (v === 'beginner' || v === 'intermediate' || v === 'expert') return v;
52
+ return null;
53
+ }
54
+
55
+ function normalizeDomainName(domain) {
56
+ const raw = String(domain || '').trim();
57
+ if (!raw) return '';
58
+ return raw.replace(/\s+/g, ' ').slice(0, 40);
59
+ }
60
+
61
+ function mergeCompetenceMap(currentMap, competenceSignals) {
62
+ const next = {
63
+ ...(currentMap && typeof currentMap === 'object' && !Array.isArray(currentMap) ? currentMap : {}),
64
+ };
65
+ const signals = Array.isArray(competenceSignals) ? competenceSignals : [];
66
+ let changed = false;
67
+
68
+ for (const signal of signals) {
69
+ const domain = normalizeDomainName(signal && signal.domain);
70
+ const level = normalizeCompetenceLevel(signal && signal.level);
71
+ if (!domain || !level) continue;
72
+
73
+ const oldLevel = normalizeCompetenceLevel(next[domain]);
74
+ if (!oldLevel) {
75
+ next[domain] = level;
76
+ changed = true;
77
+ continue;
78
+ }
79
+
80
+ const oldScore = COMPETENCE_SCORE[oldLevel] || 0;
81
+ const newScore = COMPETENCE_SCORE[level] || 0;
82
+ if (newScore >= oldScore) {
83
+ if (newScore > oldScore) {
84
+ next[domain] = level;
85
+ changed = true;
86
+ }
87
+ continue;
88
+ }
89
+
90
+ // Downgrade is only allowed with explicit downgrade evidence.
91
+ const downgradeEvidence = String(signal && signal.downgrade_evidence ? signal.downgrade_evidence : '').trim();
92
+ if (downgradeEvidence) {
93
+ next[domain] = level;
94
+ changed = true;
95
+ }
96
+ }
97
+
98
+ const entries = Object.entries(next);
99
+ if (entries.length <= 20) return { map: next, changed };
100
+
101
+ // Keep top-20 by level score to avoid unbounded growth.
102
+ entries.sort((a, b) => (COMPETENCE_SCORE[b[1]] || 0) - (COMPETENCE_SCORE[a[1]] || 0));
103
+ const compact = Object.fromEntries(entries.slice(0, 20));
104
+ return { map: compact, changed: true };
105
+ }
106
+
107
+ function sanitizeSlug(input, fallback = 'session') {
108
+ const v = String(input || '')
109
+ .trim()
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9\u4e00-\u9fff_-]+/g, '-')
112
+ .replace(/-+/g, '-')
113
+ .replace(/^-|-$/g, '');
114
+ if (!v) return fallback;
115
+ return v.slice(0, 48);
116
+ }
117
+
118
+ function stripMd(text) {
119
+ return String(text || '').replace(/[#*_`>\[\]\(\)]/g, ' ').replace(/\s+/g, ' ').trim();
120
+ }
121
+
122
+ function appendPostmortemSkillSignal(skillName, skeleton, reasons, filePath) {
123
+ let skillEvolution = null;
124
+ try { skillEvolution = require('./skill-evolution'); } catch { /* optional */ }
125
+ if (!skillEvolution || typeof skillEvolution.appendSkillSignal !== 'function') return;
126
+ const name = String(skillName || '').trim();
127
+ if (!name) return;
128
+ try {
129
+ skillEvolution.appendSkillSignal({
130
+ ts: new Date().toISOString(),
131
+ type: 'postmortem_lesson',
132
+ prompt: skeleton && skeleton.intent ? skeleton.intent : '',
133
+ output: `postmortem:${name}`,
134
+ error: null,
135
+ skills_invoked: [name],
136
+ has_tool_failure: Number(skeleton && skeleton.tool_error_count) > 0,
137
+ tool_usage: [],
138
+ files_modified: [],
139
+ cwd: skeleton && skeleton.project_path ? skeleton.project_path : '',
140
+ metadata: {
141
+ source: 'distill-postmortem',
142
+ reasons,
143
+ postmortem_file: filePath,
144
+ },
145
+ });
146
+ } catch { /* non-fatal */ }
147
+ }
148
+
149
+ async function maybeGeneratePostmortemArtifact(skeleton, sessionPath) {
150
+ if (!sessionAnalytics || !skeleton || !sessionPath) return null;
151
+ const detector = sessionAnalytics.detectSignificantSession;
152
+ if (typeof detector !== 'function') return null;
153
+
154
+ const sig = detector(skeleton);
155
+ if (!sig || !sig.significant) return null;
156
+
157
+ let evidence = null;
158
+ try {
159
+ if (typeof sessionAnalytics.extractEvidence === 'function') {
160
+ evidence = sessionAnalytics.extractEvidence(sessionPath, 3200);
161
+ }
162
+ } catch { /* non-fatal */ }
163
+
164
+ const promptInput = JSON.stringify({
165
+ skeleton: {
166
+ session_id: skeleton.session_id,
167
+ duration_min: skeleton.duration_min,
168
+ tool_error_count: skeleton.tool_error_count,
169
+ retry_sequences: skeleton.retry_sequences,
170
+ git_diff_lines: skeleton.git_diff_lines,
171
+ error_recovered: skeleton.error_recovered,
172
+ intent: skeleton.intent,
173
+ project: skeleton.project,
174
+ project_id: skeleton.project_id,
175
+ },
176
+ reasons: sig.reasons,
177
+ evidence,
178
+ }, null, 2).slice(0, 5200);
179
+
180
+ const pmPrompt = `你是工程复盘助手。根据会话骨架和证据生成一份简洁 postmortem。
181
+
182
+ 输入(JSON):
183
+ ${promptInput}
184
+
185
+ 输出 JSON:
186
+ {
187
+ "title":"简短标题",
188
+ "problem":"遇到的问题(1句)",
189
+ "root_cause":"根因(1句)",
190
+ "fix":"修复方案(1句)",
191
+ "lesson":"可复用教训(1句)",
192
+ "skill":"可选,若明显涉及某技能则写 skill 名,否则空字符串"
193
+ }
194
+
195
+ 规则:
196
+ - 只基于输入,不要虚构
197
+ - 每个字段 20-180 字
198
+ - 只输出 JSON`;
199
+
200
+ let raw = '';
201
+ try {
202
+ raw = await callHaiku(pmPrompt, distillEnv, 60000);
203
+ } catch {
204
+ return null;
205
+ }
206
+
207
+ let parsed = null;
208
+ try {
209
+ const cleaned = String(raw).replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
210
+ parsed = JSON.parse(cleaned);
211
+ } catch {
212
+ return null;
213
+ }
214
+
215
+ const title = String(parsed && parsed.title ? parsed.title : '').trim();
216
+ const problem = String(parsed && parsed.problem ? parsed.problem : '').trim();
217
+ const rootCause = String(parsed && parsed.root_cause ? parsed.root_cause : '').trim();
218
+ const fix = String(parsed && parsed.fix ? parsed.fix : '').trim();
219
+ const lesson = String(parsed && parsed.lesson ? parsed.lesson : '').trim();
220
+ if (!title || !problem || !rootCause || !fix || !lesson) return null;
221
+
222
+ fs.mkdirSync(POSTMORTEM_DIR, { recursive: true });
223
+ const day = new Date().toISOString().slice(0, 10);
224
+ const topicSlug = sanitizeSlug(skeleton.intent || title, `session-${String(skeleton.session_id || '').slice(0, 8)}`);
225
+ const filePath = path.join(POSTMORTEM_DIR, `${day}-${topicSlug}.md`);
226
+ const markdown = [
227
+ `# ${title}`,
228
+ '',
229
+ `- date: ${day}`,
230
+ `- session_id: ${skeleton.session_id || 'unknown'}`,
231
+ `- project: ${skeleton.project || 'unknown'}`,
232
+ `- project_id: ${skeleton.project_id || 'unknown'}`,
233
+ `- reasons: ${(sig.reasons || []).join(', ') || 'n/a'}`,
234
+ '',
235
+ '## 问题',
236
+ problem,
237
+ '',
238
+ '## 根因',
239
+ rootCause,
240
+ '',
241
+ '## 解法',
242
+ fix,
243
+ '',
244
+ '## 教训',
245
+ lesson,
246
+ '',
247
+ ].join('\n');
248
+ fs.writeFileSync(filePath, markdown, 'utf8');
249
+
250
+ let memory = null;
251
+ try {
252
+ memory = require('./memory');
253
+ } catch { /* optional */ }
254
+
255
+ if (memory && typeof memory.saveFacts === 'function') {
256
+ const value = stripMd(`问题:${problem} 根因:${rootCause} 解法:${fix} 教训:${lesson}`).slice(0, 280);
257
+ if (value.length >= 20) {
258
+ const entityProject = sanitizeSlug(skeleton.project || 'unknown', 'unknown');
259
+ const fallbackScope = skeleton.session_id
260
+ ? `sess_${String(skeleton.session_id).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24)}`
261
+ : null;
262
+ try {
263
+ memory.saveFacts(
264
+ skeleton.session_id || `pm-${Date.now()}`,
265
+ skeleton.project || 'unknown',
266
+ [{
267
+ entity: `postmortem.${entityProject}`,
268
+ relation: 'bug_lesson',
269
+ value,
270
+ confidence: 'high',
271
+ tags: ['postmortem'],
272
+ }],
273
+ { scope: skeleton.project_id || fallbackScope }
274
+ );
275
+ } catch { /* non-fatal */ }
276
+ }
277
+ try { memory.close(); } catch { /* non-fatal */ }
278
+ }
279
+
280
+ appendPostmortemSkillSignal(parsed && parsed.skill, skeleton, sig.reasons || [], filePath);
281
+
282
+ return { file: filePath, reasons: sig.reasons || [] };
283
+ }
284
+
40
285
  function selectSignalBatch(lines) {
41
286
  const parsed = [];
42
287
  for (const rawLine of lines) {
@@ -176,6 +421,7 @@ async function distill() {
176
421
  let sessionContext = '';
177
422
  let skeleton = null;
178
423
  let sessionSummary = null;
424
+ let targetSessionPath = null;
179
425
  if (sessionAnalytics) {
180
426
  try {
181
427
  let targetSession = null;
@@ -189,6 +435,7 @@ async function distill() {
189
435
  }
190
436
  if (targetSession) {
191
437
  skeleton = sessionAnalytics.extractSkeleton(targetSession.path);
438
+ targetSessionPath = targetSession.path;
192
439
  sessionContext = sessionAnalytics.formatForPrompt(skeleton);
193
440
  // For long sessions, extract pivot points
194
441
  sessionSummary = sessionAnalytics.summarizeSession(skeleton, targetSession.path);
@@ -364,11 +611,16 @@ BEHAVIORAL ANALYSIS — _behavior block (always output, use null if insufficient
364
611
  friction: [] # max 3 keywords describing pain points
365
612
  goal_alignment: aligned | partial | drifted | null
366
613
  drift_note: "max 30 char explanation" or null
614
+ competence_signals: # optional, max 5
615
+ - domain: "领域名"
616
+ level: beginner | intermediate | expert
617
+ evidence: "触发证据"
618
+ downgrade_evidence: "只有在明确降级时填写,否则省略"
367
619
  ${sessionContext ? '\nHint: high tool_calls + routine messages → zone likely higher. If DECLARED_GOALS exist, assess goal_alignment.' : ''}
368
620
  OUTPUT — respond with ONLY a YAML code block. If nothing worth saving AND no behavior: respond with exactly NO_UPDATE.
369
621
  Do NOT repeat existing unchanged values.`;
370
622
 
371
- // 6. Call Claude in print mode with haiku (+ provider env for relay support)
623
+ // 6. Call Claude in print mode with configured distill model (+ provider env for relay support)
372
624
  let result;
373
625
  try {
374
626
  result = await callHaiku(distillPrompt, distillEnv, 60000);
@@ -384,9 +636,23 @@ Do NOT repeat existing unchanged values.`;
384
636
 
385
637
  // 7. Parse result
386
638
  if (!result || result === 'NO_UPDATE') {
639
+ if (skeleton && sessionAnalytics) {
640
+ try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { /* non-fatal */ }
641
+ }
387
642
  ackSignals = true;
388
643
  finalize();
389
- return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
644
+ let postmortem = null;
645
+ try {
646
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
647
+ } catch { /* non-fatal */ }
648
+ return {
649
+ updated: false,
650
+ behavior: null,
651
+ skeleton,
652
+ postmortem,
653
+ signalCount: signals.length,
654
+ summary: `Analyzed ${signals.length} messages — no persistent insights found.`,
655
+ };
390
656
  }
391
657
 
392
658
  // Extract YAML block from response — require explicit code block, no fallback
@@ -420,22 +686,13 @@ Do NOT repeat existing unchanged values.`;
420
686
 
421
687
  // Schema whitelist filter: drop any keys not in schema or locked
422
688
  const filtered = filterBySchema(updates);
423
- if (Object.keys(filtered).length === 0 && !behavior) {
689
+ const extractedFieldCount = Object.keys(filtered).length;
690
+ if (extractedFieldCount === 0 && !behavior) {
424
691
  ackSignals = true;
425
692
  finalize();
426
693
  return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — all extracted fields rejected by schema.` };
427
694
  }
428
695
 
429
- // If only behavior detected but no profile updates
430
- if (Object.keys(filtered).length === 0 && behavior) {
431
- ackSignals = true;
432
- finalize();
433
- if (skeleton && sessionAnalytics) {
434
- try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { }
435
- }
436
- return { updated: false, behavior, skeleton, signalCount: signals.length, summary: `Analyzed ${signals.length} messages — behavior logged, no profile changes.` };
437
- }
438
-
439
696
  const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
440
697
 
441
698
  // Read raw content to find locked lines and comments
@@ -451,13 +708,44 @@ Do NOT repeat existing unchanged values.`;
451
708
  const merged = strategicMerge(profile, filtered, lockedKeys, pendingTraits, confidenceMap, sourceMap);
452
709
  savePending(pendingTraits);
453
710
 
711
+ // Merge competence signals into user_competence_map (upgrade by default).
712
+ const competenceSignals = Array.isArray(behavior && behavior.competence_signals)
713
+ ? behavior.competence_signals
714
+ : [];
715
+ const mergedCompetence = mergeCompetenceMap(merged.user_competence_map, competenceSignals);
716
+ if (mergedCompetence.changed) {
717
+ merged.user_competence_map = mergedCompetence.map;
718
+ }
719
+
720
+ if (extractedFieldCount === 0 && !mergedCompetence.changed) {
721
+ if (skeleton && sessionAnalytics) {
722
+ try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { }
723
+ }
724
+ ackSignals = true;
725
+ finalize();
726
+ let postmortem = null;
727
+ try {
728
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
729
+ } catch { /* non-fatal */ }
730
+ return {
731
+ updated: false,
732
+ behavior,
733
+ skeleton,
734
+ postmortem,
735
+ signalCount: signals.length,
736
+ summary: `Analyzed ${signals.length} messages — behavior logged, no profile changes.`,
737
+ };
738
+ }
739
+
454
740
  // Add distillation log entry (keep last 10, compact format)
455
741
  if (!merged.evolution) merged.evolution = {};
456
742
  if (!merged.evolution.auto_distill) merged.evolution.auto_distill = [];
743
+ const changedFields = Object.keys(filtered);
744
+ if (mergedCompetence.changed) changedFields.push('user_competence_map');
457
745
  merged.evolution.auto_distill.push({
458
746
  ts: new Date().toISOString(),
459
747
  signals: signals.length,
460
- fields: Object.keys(filtered).join(', ')
748
+ fields: changedFields.join(', ')
461
749
  });
462
750
  // Cap at 10 entries
463
751
  if (merged.evolution.auto_distill.length > 10) {
@@ -507,13 +795,19 @@ Do NOT repeat existing unchanged values.`;
507
795
 
508
796
  ackSignals = true;
509
797
  finalize();
798
+ let postmortem = null;
799
+ try {
800
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
801
+ } catch { /* non-fatal */ }
802
+ const absorbedCount = changedFields.length;
510
803
  return {
511
804
  updated: true,
512
805
  behavior,
513
806
  skeleton,
514
807
  sessionSummary,
808
+ postmortem,
515
809
  signalCount: signals.length,
516
- summary: `${Object.keys(filtered).length} new trait${Object.keys(filtered).length > 1 ? 's' : ''} absorbed. (${tokens} tokens)`
810
+ summary: `${absorbedCount} new trait${absorbedCount > 1 ? 's' : ''} absorbed. (${tokens} tokens)`
517
811
  };
518
812
 
519
813
  } catch (err) {
@@ -1077,7 +1371,18 @@ If no clear patterns found: respond with exactly NO_PATTERNS`;
1077
1371
  }
1078
1372
 
1079
1373
  // Export for use in index.js
1080
- module.exports = { distill, writeSessionLog, bootstrapSessionLog, detectPatterns };
1374
+ module.exports = {
1375
+ distill,
1376
+ writeSessionLog,
1377
+ bootstrapSessionLog,
1378
+ detectPatterns,
1379
+ _private: {
1380
+ mergeCompetenceMap,
1381
+ normalizeCompetenceLevel,
1382
+ normalizeDomainName,
1383
+ sanitizeSlug,
1384
+ },
1385
+ };
1081
1386
 
1082
1387
  // Also allow direct execution
1083
1388
  if (require.main === module) {
@@ -1104,5 +1409,9 @@ if (require.main === module) {
1104
1409
  } else {
1105
1410
  console.log(`💤 ${result.summary}`);
1106
1411
  }
1412
+ // Report estimated token usage for daemon budget tracking
1413
+ // Each callHaiku invocation ~2k-5k tokens; estimate from signal count + result size
1414
+ const estTokens = Math.ceil(((result.signalCount || 1) * 500) + ((result.summary || '').length / 4));
1415
+ console.log(`__TOKENS__:${estTokens}`);
1107
1416
  })();
1108
1417
  }
@@ -16,6 +16,15 @@
16
16
  在手机端(飞书/Telegram),直接说即可,daemon 会自动处理:
17
17
  > 创建一个 Agent,目录是 ~/projects/my-bot
18
18
 
19
+ 引擎选择(手机端自然语言):
20
+ - 默认不写引擎时,使用 Claude(配置里不落 `engine` 字段)
21
+ - 句子里带 `codex` 关键词时,自动写入 `engine: codex`
22
+
23
+ 示例:
24
+ > 创建一个 codex agent,目录是 ~/projects/reviewer
25
+
26
+ > 用 codex 建一个代码审查 agent,目录 ~/projects/pr-review
27
+
19
28
  在桌面 Claude Code 终端,需要手动操作:
20
29
  1. 创建项目目录和 CLAUDE.md(角色定义)
21
30
  2. 编辑 `~/.metame/daemon.yaml`,在 `projects` 下新增:
@@ -48,3 +57,6 @@
48
57
  ## 注意事项
49
58
  - 专属群(chat_agent_map 中的群)永远绑定同一个 Agent,不能通过昵称切换
50
59
  - 新群必须发 `/activate` 才能使用,未授权群会提示"此群未授权"
60
+ - Codex 当前限制(MVP):`/sessions` 列表暂只展示 Claude 本地会话,Codex 会话暂不可见
61
+ - Codex 当前限制(MVP):`/compact` 暂不支持,请继续在同一会话中对话
62
+ - 需要定位脚本入口、升级步骤或文件落点时,先看 `~/.metame/docs/pointer-map.md`
@@ -0,0 +1,155 @@
1
+ # MetaMe 维护手册(Claude/Codex 双引擎)
2
+
3
+ > 适用范围:`scripts/daemon.js` 后台 daemon 链路(飞书/Telegram 路由、会话执行、Agent 绑定)。
4
+
5
+ ## 1. 引擎路由规则
6
+
7
+ ### 手机端(daemon 生效)
8
+
9
+ - 路由入口:`chat_agent_map -> project -> project.engine`
10
+ - `project.engine` 可选值:`claude`(默认)/`codex`
11
+ - 未配置 `engine` 时等价 `claude`
12
+
13
+ 示例:
14
+
15
+ ```yaml
16
+ projects:
17
+ reviewer:
18
+ name: "Reviewer"
19
+ cwd: "~/AGI/MetaMe"
20
+ engine: codex
21
+
22
+ feishu:
23
+ chat_agent_map:
24
+ "oc_xxx": "reviewer"
25
+ ```
26
+
27
+ ### 电脑端(CLI)
28
+
29
+ - Claude 入口:`metame`(等价启动 Claude + MetaMe 初始化)
30
+ - Codex 入口:`metame codex [args]`
31
+ - 也可直接用原生命令:`claude` / `codex`
32
+
33
+ ## 2. Agent 创建与引擎写入
34
+
35
+ - 默认创建 Agent:不写 `engine` 字段(保持兼容)
36
+ - 创建语句中包含 `codex` 关键词:写入 `engine: codex`
37
+ - 绑定语句包含 `codex` 时同样支持写入 `engine: codex`
38
+
39
+ 示例:
40
+
41
+ - `创建一个 codex agent,目录是 ~/projects/reviewer`
42
+ - `用 codex 建个代码审查 agent,目录 ~/projects/pr-review`
43
+
44
+ ## 3. 会话与执行规则
45
+
46
+ - 引擎 runtime 工厂:`scripts/daemon-engine-runtime.js`
47
+ - 会话执行入口:`scripts/daemon-claude-engine.js`(Claude/Codex 共用)
48
+ - Session 回写:`patchSessionSerialized()` 串行化,避免 thread.started 竞态覆盖
49
+
50
+ ### Codex 会话策略
51
+
52
+ - 首轮:`codex exec --json -`
53
+ - 续轮:`codex exec resume <thread_id> --json -`
54
+ - `resume` 失败自动重试:同一 `chatId` 在 10 分钟内最多 1 次
55
+ - 收到新 `thread_id` 时自动迁移 session id
56
+
57
+ ## 4. 命令行为差异
58
+
59
+ - `/stop`:引擎中性,按 `activeProcesses.killSignal` 停止
60
+ - `/compact`:
61
+ - Claude 会话:正常压缩
62
+ - Codex 会话:返回“暂不支持,请继续同会话”
63
+ - `/engine`:
64
+ - 查询当前默认引擎:`/engine`
65
+ - 切换默认引擎:`/engine claude` 或 `/engine codex`
66
+ - `/distill-model`:
67
+ - 查询当前蒸馏模型:`/distill-model`
68
+ - 设置蒸馏模型:`/distill-model gpt-5.1-codex-mini`
69
+ - 也支持严格自然语言:`把蒸馏模型改成 5.1mini`
70
+ - `/doctor`:
71
+ - 同时检查 Claude/Codex CLI 可用性
72
+ - 仅在“当前默认引擎对应 CLI 不可用”时判为故障
73
+ - 自定义 provider 下允许任意合法模型名(不再强制 sonnet/opus/haiku)
74
+
75
+ ## 5. Agent Soul 身份层
76
+
77
+ - 集中存储:`~/.metame/agents/<agent_id>/`(soul.md、memory-snapshot.md、agent.yaml)
78
+ - 项目视图:`<cwd>/SOUL.md`、`<cwd>/MEMORY.md` 是指向集中存储的链接
79
+ - Claude:`@SOUL.md` 写入 CLAUDE.md 头部,CLI 每次 session 自动加载(引用式,改源文件立即生效)
80
+ - Codex:每次新 session 合并 CLAUDE.md + SOUL.md 写入 `<cwd>/AGENTS.md`(快照式,需新 session 生效)
81
+ - 老项目迁移:`/agent soul repair` 幂等补建 soul 层
82
+ - 注意:Windows 上 copy 模式的 SOUL.md 不会自动同步源文件变更,需 `/agent soul repair` 刷新
83
+
84
+ ## 6. 运行时文件与状态
85
+
86
+ - 配置:`~/.metame/daemon.yaml`
87
+ - daemon 状态:`~/.metame/daemon_state.json`
88
+ - 活跃子进程:`~/.metame/active_agent_pids.json`
89
+ - 热重载备份:`~/.metame/.last-good/`(daemon 稳定运行 60s 后自动备份)
90
+ - 崩溃计数:`~/.metame/.crash-count`(连续 2 次快速崩溃触发自动恢复)
91
+
92
+ ## 7. 热重载安全机制(三层防护)
93
+
94
+ 1. **部署前预检**(`index.js`):`node -c` 语法检查所有 `.js`,不通过则拒绝部署到 `~/.metame/`
95
+ 2. **重启前预检**(`daemon-runtime-lifecycle.js`):daemon.js 变更触发重启前再次语法校验,不通过则阻止重启并通知 admin
96
+ 3. **崩溃循环自愈**:连续 2 次在 30s 内崩溃 → 自动从 `.last-good/` 恢复 → 通知 admin
97
+
98
+ ## 8. 常见故障排查
99
+
100
+ ### Codex 认证失败
101
+
102
+ 症状:返回 `AUTH_REQUIRED`
103
+
104
+ 处理:
105
+
106
+ 1. 执行 `codex login`
107
+ 2. 或配置 `OPENAI_API_KEY`
108
+ 3. 重新发送同一条消息
109
+
110
+ ### Codex 频率限制
111
+
112
+ 症状:返回 `RATE_LIMIT`
113
+
114
+ 处理:
115
+
116
+ 1. 等待限流窗口恢复
117
+ 2. 降低并发或切回 Claude 路由
118
+
119
+ ### 会话续接异常
120
+
121
+ 症状:Codex `resume` 报错后反复失败
122
+
123
+ 处理:
124
+
125
+ 1. daemon 已自动做一次 fresh `exec` 重试
126
+ 2. 若仍失败,手动 `/new` 新开会话
127
+ 3. 检查 `~/.metame/active_agent_pids.json` 是否残留异常进程
128
+
129
+ ## 9. 双平台/双引擎维护矩阵
130
+
131
+ ### 统一维护(改一处即可)
132
+ - agent-layer.js / daemon-agent-tools.js / daemon-agent-commands.js / daemon-user-acl.js
133
+ - ENGINE_MODEL_CONFIG(daemon-engine-runtime.js 集中管理)
134
+ - daemon-runtime-lifecycle.js 的语法检查和备份机制
135
+
136
+ ### 需分别维护(有平台/引擎特殊分支)
137
+
138
+ | 模块 | 差异点 | 注意事项 |
139
+ |------|--------|----------|
140
+ | platform.js `killProcessTree` | POSIX: `kill(-pid)` / Windows: `taskkill /T /F` | 所有进程杀死调用点应统一使用此函数 |
141
+ | daemon-engine-runtime.js `resolveBinary` | macOS: `which` + homebrew / Windows: `where` + `.cmd` | 新增引擎需两端测试 |
142
+ | daemon-engine-runtime.js `buildArgs` | Claude: `--resume`/`--continue` / Codex: `exec resume`,Codex resume 不能传权限 flag | 改参数结构时两端验证 |
143
+ | daemon-claude-engine.js Soul 注入 | Claude: `@SOUL.md` import(引用式)/ Codex: AGENTS.md 合并写入(快照式) | 改 soul 加载方式需两端测试 |
144
+ | agent-layer.js `createLinkOrMirror` | macOS: symlink / Windows: hardlink → copy 降级 | copy 模式不会自动同步源文件变更 |
145
+ | daemon.js `spawnReplacementDaemon` | POSIX: `detached: true` / Windows: `detached: false` | 改 spawn 参数时注意平台分支 |
146
+ | NL Mac 控制(command-router) | macOS only,`process.platform === 'darwin'` 守卫 | Windows 天然跳过 |
147
+
148
+ ## 10. 变更后维护动作
149
+
150
+ 1. `npm test`
151
+ 2. `npm run sync:plugin`
152
+ 3. 更新文档:
153
+ - `scripts/docs/pointer-map.md`
154
+ - `README.md`
155
+ - `README中文版.md`