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.
- package/README.md +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- 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/telegram-adapter.js +12 -8
- 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) {
|
|
@@ -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`
|