metame-cli 1.4.33 → 1.5.0
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 +187 -48
- package/index.js +148 -9
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +254 -9
- package/scripts/daemon-agent-commands.js +64 -6
- package/scripts/daemon-agent-tools.js +26 -5
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-claude-engine.js +704 -268
- package/scripts/daemon-command-router.js +24 -8
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +275 -0
- package/scripts/daemon-exec-commands.js +10 -4
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-runtime-lifecycle.js +2 -1
- package/scripts/daemon-session-commands.js +52 -4
- package/scripts/daemon-session-store.js +2 -1
- package/scripts/daemon-task-scheduler.js +87 -28
- package/scripts/daemon-user-acl.js +26 -9
- package/scripts/daemon.js +81 -17
- package/scripts/distill.js +323 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +119 -0
- package/scripts/docs/pointer-map.md +88 -0
- package/scripts/feishu-adapter.js +6 -1
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +100 -5
- package/scripts/memory-nightly-reflect.js +196 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +2 -0
- package/scripts/providers.js +169 -21
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
package/scripts/distill.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MetaMe Passive Distiller
|
|
5
5
|
*
|
|
6
|
-
* Reads raw signal buffer, calls Claude (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: `${
|
|
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 = {
|
|
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) {
|
|
@@ -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,119 @@
|
|
|
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. 运行时文件与状态
|
|
76
|
+
|
|
77
|
+
- 配置:`~/.metame/daemon.yaml`
|
|
78
|
+
- daemon 状态:`~/.metame/daemon_state.json`
|
|
79
|
+
- 活跃子进程:`~/.metame/active_agent_pids.json`
|
|
80
|
+
|
|
81
|
+
## 6. 常见故障排查
|
|
82
|
+
|
|
83
|
+
### Codex 认证失败
|
|
84
|
+
|
|
85
|
+
症状:返回 `AUTH_REQUIRED`
|
|
86
|
+
|
|
87
|
+
处理:
|
|
88
|
+
|
|
89
|
+
1. 执行 `codex login`
|
|
90
|
+
2. 或配置 `OPENAI_API_KEY`
|
|
91
|
+
3. 重新发送同一条消息
|
|
92
|
+
|
|
93
|
+
### Codex 频率限制
|
|
94
|
+
|
|
95
|
+
症状:返回 `RATE_LIMIT`
|
|
96
|
+
|
|
97
|
+
处理:
|
|
98
|
+
|
|
99
|
+
1. 等待限流窗口恢复
|
|
100
|
+
2. 降低并发或切回 Claude 路由
|
|
101
|
+
|
|
102
|
+
### 会话续接异常
|
|
103
|
+
|
|
104
|
+
症状:Codex `resume` 报错后反复失败
|
|
105
|
+
|
|
106
|
+
处理:
|
|
107
|
+
|
|
108
|
+
1. daemon 已自动做一次 fresh `exec` 重试
|
|
109
|
+
2. 若仍失败,手动 `/new` 新开会话
|
|
110
|
+
3. 检查 `~/.metame/active_agent_pids.json` 是否残留异常进程
|
|
111
|
+
|
|
112
|
+
## 7. 变更后维护动作
|
|
113
|
+
|
|
114
|
+
1. `npm test`
|
|
115
|
+
2. `npm run sync:plugin`
|
|
116
|
+
3. 更新文档:
|
|
117
|
+
- `scripts/docs/pointer-map.md`
|
|
118
|
+
- `README.md`
|
|
119
|
+
- `README中文版.md`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# MetaMe 脚本/文档指针地图
|
|
2
|
+
|
|
3
|
+
> 目的:回答“这段能力在哪个文件”“当前升级做到哪一步”“先看哪个脚本”。
|
|
4
|
+
|
|
5
|
+
## 快速入口
|
|
6
|
+
|
|
7
|
+
- 主入口:`index.js`
|
|
8
|
+
- CLI 双入口:`metame`(Claude)/`metame codex [args]`(Codex)
|
|
9
|
+
- Daemon 主循环:`scripts/daemon.js`
|
|
10
|
+
- 多引擎 runtime 适配层:`scripts/daemon-engine-runtime.js`
|
|
11
|
+
- 会话执行引擎(Claude/Codex 共用入口):`scripts/daemon-claude-engine.js`
|
|
12
|
+
- 管理命令:`scripts/daemon-admin-commands.js`
|
|
13
|
+
- 命令路由:`scripts/daemon-command-router.js`
|
|
14
|
+
- 执行命令(`/stop`、`/compact` 等):`scripts/daemon-exec-commands.js`
|
|
15
|
+
- 会话存储:`scripts/daemon-session-store.js`
|
|
16
|
+
- 默认配置:`scripts/daemon-default.yaml`
|
|
17
|
+
- Provider/蒸馏模型配置:`scripts/providers.js`(`/provider`、`/distill-model`)
|
|
18
|
+
- 维护手册:`scripts/docs/maintenance-manual.md`
|
|
19
|
+
|
|
20
|
+
## 多引擎(Claude/Codex)定位
|
|
21
|
+
|
|
22
|
+
- Runtime 工厂与事件归一化:
|
|
23
|
+
- `scripts/daemon-engine-runtime.js`
|
|
24
|
+
- 关键点:`normalizeEngineName()`、`buildClaudeArgs()`、`buildCodexArgs()`、`parseCodexStreamEvent()`
|
|
25
|
+
|
|
26
|
+
- 会话与引擎选择:
|
|
27
|
+
- `scripts/daemon-claude-engine.js`
|
|
28
|
+
- 关键点:`askClaude()` 按 `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
|
|
29
|
+
- Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
|
|
30
|
+
|
|
31
|
+
- 路由与 Agent 创建:
|
|
32
|
+
- `scripts/daemon-command-router.js`
|
|
33
|
+
- `scripts/daemon-agent-tools.js`
|
|
34
|
+
- 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`
|
|
35
|
+
|
|
36
|
+
- 会话命令与兼容边界:
|
|
37
|
+
- `scripts/daemon-exec-commands.js`
|
|
38
|
+
- 关键点:`/stop` 引擎中性;`/compact` 在 codex 会话返回“暂不支持”
|
|
39
|
+
|
|
40
|
+
- 运行时引擎切换与诊断:
|
|
41
|
+
- `scripts/daemon-admin-commands.js`
|
|
42
|
+
- 关键点:`/engine` 切换默认引擎;`/doctor` 按默认引擎检查 CLI 可用性(Claude/Codex)并兼容自定义 provider 模型名
|
|
43
|
+
|
|
44
|
+
## Mentor Mode(Step 1-4)定位
|
|
45
|
+
|
|
46
|
+
- Step 1 数据基建:
|
|
47
|
+
- `scripts/session-analytics.js`
|
|
48
|
+
- 关键点:`extractSkeleton()` 新增数值指标、`detectSignificantSession()`
|
|
49
|
+
- `scripts/schema.js`:`growth.mentor_mode`、`growth.mentor_friction_level`、`growth.weekly_report_last`
|
|
50
|
+
- `scripts/memory.js`:`fact_labels` 表结构
|
|
51
|
+
|
|
52
|
+
- Step 2 决策引擎:
|
|
53
|
+
- `scripts/mentor-engine.js`
|
|
54
|
+
- 关键 API:`checkEmotionBreaker` / `buildMentorPrompt` / `computeZone` / `registerDebt` / `collectDebt` / `detectPatterns`
|
|
55
|
+
- 运行时状态文件:`~/.metame/mentor_runtime.json`
|
|
56
|
+
|
|
57
|
+
- Step 3 Hook 接入:
|
|
58
|
+
- `scripts/daemon-claude-engine.js`:Pre-flight / Context / Post-flight 三段 Hook
|
|
59
|
+
- `scripts/daemon-admin-commands.js`:`/mentor on|off|level|status`
|
|
60
|
+
- `scripts/daemon-default.yaml`:`daemon.mentor` 配置段
|
|
61
|
+
|
|
62
|
+
- Step 4 Distiller & Memory 闭环:
|
|
63
|
+
- `scripts/distill.js`:`competence_signals` 合并、significant session postmortem 产出、`bug_lesson` 回写
|
|
64
|
+
- `scripts/memory-extract.js`:消费 `saveFacts().savedFacts`,写入 `fact_labels`
|
|
65
|
+
- `scripts/memory.js`:`saveFactLabels()` 原子写入 API
|
|
66
|
+
- `scripts/memory-nightly-reflect.js`:`synthesized_insight` 回写、知识胶囊聚合与 `knowledge_capsule` 回写
|
|
67
|
+
|
|
68
|
+
## 运行时数据位置
|
|
69
|
+
|
|
70
|
+
- 画像:`~/.claude_profile.yaml`
|
|
71
|
+
- 记忆数据库:`~/.metame/memory.db`
|
|
72
|
+
- 会话标签:`~/.metame/session_tags.json`
|
|
73
|
+
- 进程 PID 记录:`~/.metame/active_agent_pids.json`
|
|
74
|
+
- 夜间反思文档:`~/.metame/memory/decisions/`、`~/.metame/memory/lessons/`
|
|
75
|
+
- 知识胶囊:`~/.metame/memory/capsules/`
|
|
76
|
+
- 复盘文档:`~/.metame/memory/postmortems/`
|
|
77
|
+
|
|
78
|
+
## 诊断顺序(推荐)
|
|
79
|
+
|
|
80
|
+
1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
|
|
81
|
+
2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
|
|
82
|
+
3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
|
|
83
|
+
4. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
|
|
84
|
+
|
|
85
|
+
## 同步提示
|
|
86
|
+
|
|
87
|
+
- 每次改 `scripts/` 后执行:`npm run sync:plugin`
|
|
88
|
+
- plugin 镜像路径:`plugin/scripts/*`
|
|
@@ -416,10 +416,15 @@ function createBot(config) {
|
|
|
416
416
|
const chatId = data.open_chat_id || data.chat_id
|
|
417
417
|
|| (data.context && data.context.open_chat_id)
|
|
418
418
|
|| (data.event && data.event.open_chat_id);
|
|
419
|
+
const senderId = (data.operator && data.operator.open_id)
|
|
420
|
+
|| (data.open_id)
|
|
421
|
+
|| (data.user && data.user.open_id)
|
|
422
|
+
|| (data.context && data.context.open_id)
|
|
423
|
+
|| null;
|
|
419
424
|
if (action && chatId) {
|
|
420
425
|
const cmd = action.value && action.value.cmd;
|
|
421
426
|
if (cmd) {
|
|
422
|
-
Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch((err) => {
|
|
427
|
+
Promise.resolve().then(() => onMessage(chatId, cmd, data, null, senderId)).catch((err) => {
|
|
423
428
|
try { console.error(`[feishu-adapter] card action error: ${err && err.message || err}`); } catch { }
|
|
424
429
|
});
|
|
425
430
|
}
|