metame-cli 1.5.19 → 1.5.21
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/index.js +157 -80
- package/package.json +2 -2
- package/scripts/bin/bootstrap-worktree.sh +20 -0
- package/scripts/core/audit.js +190 -0
- package/scripts/core/handoff.js +780 -0
- package/scripts/core/handoff.test.js +1074 -0
- package/scripts/core/memory-model.js +183 -0
- package/scripts/core/memory-model.test.js +486 -0
- package/scripts/core/reactive-paths.js +44 -0
- package/scripts/core/reactive-paths.test.js +35 -0
- package/scripts/core/reactive-prompt.js +51 -0
- package/scripts/core/reactive-prompt.test.js +88 -0
- package/scripts/core/reactive-signal.js +40 -0
- package/scripts/core/reactive-signal.test.js +88 -0
- package/scripts/core/thread-chat-id.js +52 -0
- package/scripts/core/thread-chat-id.test.js +113 -0
- package/scripts/daemon-bridges.js +92 -38
- package/scripts/daemon-claude-engine.js +373 -444
- package/scripts/daemon-command-router.js +82 -8
- package/scripts/daemon-engine-runtime.js +7 -10
- package/scripts/daemon-reactive-lifecycle.js +100 -33
- package/scripts/daemon-session-commands.js +133 -43
- package/scripts/daemon-session-store.js +300 -82
- package/scripts/daemon-team-dispatch.js +16 -16
- package/scripts/daemon.js +21 -175
- package/scripts/deploy-manifest.js +90 -0
- package/scripts/docs/maintenance-manual.md +14 -11
- package/scripts/docs/pointer-map.md +13 -4
- package/scripts/feishu-adapter.js +31 -27
- package/scripts/hooks/intent-engine.js +6 -3
- package/scripts/hooks/intent-memory-recall.js +1 -0
- package/scripts/hooks/intent-perpetual.js +1 -1
- package/scripts/memory-extract.js +5 -97
- package/scripts/memory-gc.js +35 -90
- package/scripts/memory-migrate-v2.js +304 -0
- package/scripts/memory-nightly-reflect.js +40 -41
- package/scripts/memory.js +340 -859
- package/scripts/migrate-reactive-paths.js +122 -0
- package/scripts/signal-capture.js +4 -0
- package/scripts/sync-plugin.js +56 -0
package/scripts/daemon.js
CHANGED
|
@@ -138,6 +138,7 @@ const {
|
|
|
138
138
|
USAGE_RETENTION_DAYS_DEFAULT,
|
|
139
139
|
normalizeUsageCategory,
|
|
140
140
|
} = require('./usage-classifier');
|
|
141
|
+
const { createAudit } = require('./core/audit');
|
|
141
142
|
const { createTaskBoard } = require('./task-board');
|
|
142
143
|
const taskEnvelope = require('./daemon-task-envelope');
|
|
143
144
|
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
@@ -196,38 +197,22 @@ function getActiveProviderEnv() {
|
|
|
196
197
|
try { return providerMod.buildActiveEnv(); } catch { return {}; }
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const bakFile = LOG_FILE + '.bak';
|
|
216
|
-
if (fs.existsSync(bakFile)) fs.unlinkSync(bakFile);
|
|
217
|
-
fs.renameSync(LOG_FILE, bakFile);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
fs.appendFileSync(LOG_FILE, line, 'utf8');
|
|
221
|
-
} catch {
|
|
222
|
-
// Last resort
|
|
223
|
-
process.stderr.write(line);
|
|
224
|
-
}
|
|
225
|
-
// When running as LaunchAgent (stdout redirected to file), mirror structured logs there too.
|
|
226
|
-
// This unifies daemon.log and daemon-npm-stdout.log into one source of truth.
|
|
227
|
-
if (!process.stdout.isTTY) {
|
|
228
|
-
process.stdout.write(line);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
200
|
+
const {
|
|
201
|
+
refreshLogMaxSize,
|
|
202
|
+
log,
|
|
203
|
+
loadState,
|
|
204
|
+
saveState,
|
|
205
|
+
ensureUsageShape,
|
|
206
|
+
ensureStateShape,
|
|
207
|
+
pruneDailyUsage,
|
|
208
|
+
} = createAudit({
|
|
209
|
+
fs,
|
|
210
|
+
logFile: LOG_FILE,
|
|
211
|
+
stateFile: STATE_FILE,
|
|
212
|
+
stdout: process.stdout,
|
|
213
|
+
stderr: process.stderr,
|
|
214
|
+
usageRetentionDaysDefault: USAGE_RETENTION_DAYS_DEFAULT,
|
|
215
|
+
});
|
|
231
216
|
|
|
232
217
|
const {
|
|
233
218
|
cpExtractTimestamp,
|
|
@@ -337,149 +322,6 @@ function restoreConfig() {
|
|
|
337
322
|
}
|
|
338
323
|
}
|
|
339
324
|
|
|
340
|
-
let _cachedState = null;
|
|
341
|
-
|
|
342
|
-
function ensureUsageShape(state) {
|
|
343
|
-
if (!state.usage || typeof state.usage !== 'object') state.usage = {};
|
|
344
|
-
if (!state.usage.categories || typeof state.usage.categories !== 'object') state.usage.categories = {};
|
|
345
|
-
if (!state.usage.daily || typeof state.usage.daily !== 'object') state.usage.daily = {};
|
|
346
|
-
const keepDays = Number(state.usage.retention_days);
|
|
347
|
-
state.usage.retention_days = Number.isFinite(keepDays) && keepDays >= 7
|
|
348
|
-
? Math.floor(keepDays)
|
|
349
|
-
: USAGE_RETENTION_DAYS_DEFAULT;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function ensureStateShape(state) {
|
|
353
|
-
if (!state || typeof state !== 'object') return {
|
|
354
|
-
pid: null,
|
|
355
|
-
budget: { date: null, tokens_used: 0 },
|
|
356
|
-
tasks: {},
|
|
357
|
-
sessions: {},
|
|
358
|
-
started_at: null,
|
|
359
|
-
usage: { retention_days: USAGE_RETENTION_DAYS_DEFAULT, categories: {}, daily: {} },
|
|
360
|
-
};
|
|
361
|
-
if (!state.budget || typeof state.budget !== 'object') state.budget = { date: null, tokens_used: 0 };
|
|
362
|
-
if (typeof state.budget.tokens_used !== 'number') state.budget.tokens_used = Number(state.budget.tokens_used) || 0;
|
|
363
|
-
if (!Object.prototype.hasOwnProperty.call(state.budget, 'date')) state.budget.date = null;
|
|
364
|
-
if (!state.tasks || typeof state.tasks !== 'object') state.tasks = {};
|
|
365
|
-
if (!state.sessions || typeof state.sessions !== 'object') state.sessions = {};
|
|
366
|
-
ensureUsageShape(state);
|
|
367
|
-
return state;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function pruneDailyUsage(usage, todayIso) {
|
|
371
|
-
const keepDays = usage.retention_days || USAGE_RETENTION_DAYS_DEFAULT;
|
|
372
|
-
const cutoff = new Date(`${todayIso}T00:00:00.000Z`);
|
|
373
|
-
cutoff.setUTCDate(cutoff.getUTCDate() - (keepDays - 1));
|
|
374
|
-
const cutoffIso = cutoff.toISOString().slice(0, 10);
|
|
375
|
-
for (const day of Object.keys(usage.daily || {})) {
|
|
376
|
-
if (day < cutoffIso) delete usage.daily[day];
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function _readStateFromDisk() {
|
|
381
|
-
try {
|
|
382
|
-
const s = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
383
|
-
return ensureStateShape(s);
|
|
384
|
-
} catch {
|
|
385
|
-
return ensureStateShape({
|
|
386
|
-
pid: null,
|
|
387
|
-
budget: { date: null, tokens_used: 0 },
|
|
388
|
-
tasks: {},
|
|
389
|
-
sessions: {},
|
|
390
|
-
started_at: null,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function loadState() {
|
|
396
|
-
if (!_cachedState) _cachedState = _readStateFromDisk();
|
|
397
|
-
return _cachedState;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function saveState(state) {
|
|
401
|
-
const next = ensureStateShape(state);
|
|
402
|
-
if (_cachedState && _cachedState !== next) {
|
|
403
|
-
const current = ensureStateShape(_cachedState);
|
|
404
|
-
|
|
405
|
-
const currentBudgetDate = String(current.budget.date || '');
|
|
406
|
-
const nextBudgetDate = String(next.budget.date || '');
|
|
407
|
-
const currentBudgetTokens = Math.max(0, Math.floor(Number(current.budget.tokens_used) || 0));
|
|
408
|
-
const nextBudgetTokens = Math.max(0, Math.floor(Number(next.budget.tokens_used) || 0));
|
|
409
|
-
if (currentBudgetDate && (!nextBudgetDate || currentBudgetDate > nextBudgetDate)) {
|
|
410
|
-
next.budget.date = currentBudgetDate;
|
|
411
|
-
next.budget.tokens_used = currentBudgetTokens;
|
|
412
|
-
} else if (currentBudgetDate && currentBudgetDate === nextBudgetDate) {
|
|
413
|
-
next.budget.tokens_used = Math.max(currentBudgetTokens, nextBudgetTokens);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const currentKeepDays = Number(current.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
|
|
417
|
-
const nextKeepDays = Number(next.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
|
|
418
|
-
next.usage.retention_days = Math.max(currentKeepDays, nextKeepDays);
|
|
419
|
-
|
|
420
|
-
for (const [category, curMeta] of Object.entries(current.usage.categories || {})) {
|
|
421
|
-
if (!next.usage.categories[category] || typeof next.usage.categories[category] !== 'object') {
|
|
422
|
-
next.usage.categories[category] = {};
|
|
423
|
-
}
|
|
424
|
-
const curTotal = Math.max(0, Math.floor(Number(curMeta && curMeta.total) || 0));
|
|
425
|
-
const nextTotal = Math.max(0, Math.floor(Number(next.usage.categories[category].total) || 0));
|
|
426
|
-
if (curTotal > nextTotal) next.usage.categories[category].total = curTotal;
|
|
427
|
-
|
|
428
|
-
const curUpdated = String(curMeta && curMeta.updated_at || '');
|
|
429
|
-
const nextUpdated = String(next.usage.categories[category].updated_at || '');
|
|
430
|
-
if (curUpdated && curUpdated > nextUpdated) next.usage.categories[category].updated_at = curUpdated;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
for (const [day, curDayUsageRaw] of Object.entries(current.usage.daily || {})) {
|
|
434
|
-
const curDayUsage = (curDayUsageRaw && typeof curDayUsageRaw === 'object') ? curDayUsageRaw : {};
|
|
435
|
-
if (!next.usage.daily[day] || typeof next.usage.daily[day] !== 'object') {
|
|
436
|
-
next.usage.daily[day] = {};
|
|
437
|
-
}
|
|
438
|
-
const nextDayUsage = next.usage.daily[day];
|
|
439
|
-
for (const [key, curValue] of Object.entries(curDayUsage)) {
|
|
440
|
-
const curNum = Math.max(0, Math.floor(Number(curValue) || 0));
|
|
441
|
-
const nextNum = Math.max(0, Math.floor(Number(nextDayUsage[key]) || 0));
|
|
442
|
-
if (curNum > nextNum) nextDayUsage[key] = curNum;
|
|
443
|
-
}
|
|
444
|
-
const categorySum = Object.entries(nextDayUsage)
|
|
445
|
-
.filter(([key]) => key !== 'total')
|
|
446
|
-
.reduce((sum, [, value]) => sum + Math.max(0, Math.floor(Number(value) || 0)), 0);
|
|
447
|
-
nextDayUsage.total = Math.max(Math.max(0, Math.floor(Number(nextDayUsage.total) || 0)), categorySum);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const currentUsageUpdated = String(current.usage.updated_at || '');
|
|
451
|
-
const nextUsageUpdated = String(next.usage.updated_at || '');
|
|
452
|
-
if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
|
|
453
|
-
next.usage.updated_at = currentUsageUpdated;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Merge sessions: prevent concurrent agents from wiping each other's session data.
|
|
457
|
-
// When a stale state object is saved (e.g. after a long spawnClaudeStreaming await),
|
|
458
|
-
// preserve any sessions that were added/updated by other agents in the interim.
|
|
459
|
-
if (current.sessions && typeof current.sessions === 'object') {
|
|
460
|
-
if (!next.sessions || typeof next.sessions !== 'object') next.sessions = {};
|
|
461
|
-
for (const [key, curSession] of Object.entries(current.sessions)) {
|
|
462
|
-
if (!next.sessions[key]) {
|
|
463
|
-
// Session exists in cache but not in incoming state → preserve it
|
|
464
|
-
next.sessions[key] = curSession;
|
|
465
|
-
} else {
|
|
466
|
-
// Both have it → keep whichever has newer last_active
|
|
467
|
-
const curActive = Number(curSession && curSession.last_active) || 0;
|
|
468
|
-
const nextActive = Number(next.sessions[key] && next.sessions[key].last_active) || 0;
|
|
469
|
-
if (curActive > nextActive) next.sessions[key] = curSession;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
_cachedState = next;
|
|
476
|
-
try {
|
|
477
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
478
|
-
} catch (e) {
|
|
479
|
-
log('ERROR', `Failed to save state: ${e.message}`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
325
|
// ---------------------------------------------------------
|
|
484
326
|
// PROFILE PREAMBLE (lightweight — only core fields for daemon)
|
|
485
327
|
// ---------------------------------------------------------
|
|
@@ -1911,7 +1753,9 @@ const {
|
|
|
1911
1753
|
findCodexSessionFile,
|
|
1912
1754
|
clearSessionFileCache,
|
|
1913
1755
|
truncateSessionToCheckpoint,
|
|
1756
|
+
stripThinkingSignatures,
|
|
1914
1757
|
listRecentSessions,
|
|
1758
|
+
findAttachableSession,
|
|
1915
1759
|
loadSessionTags,
|
|
1916
1760
|
getSessionFileMtime,
|
|
1917
1761
|
sessionLabel,
|
|
@@ -2157,6 +2001,7 @@ const { handleSessionCommand } = createSessionCommandHandler({
|
|
|
2157
2001
|
getCachedFile,
|
|
2158
2002
|
getSession,
|
|
2159
2003
|
listRecentSessions,
|
|
2004
|
+
findAttachableSession,
|
|
2160
2005
|
getSessionFileMtime,
|
|
2161
2006
|
formatRelativeTime,
|
|
2162
2007
|
sendDirListing,
|
|
@@ -2211,6 +2056,7 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
2211
2056
|
getSessionName,
|
|
2212
2057
|
writeSessionName,
|
|
2213
2058
|
markSessionStarted,
|
|
2059
|
+
stripThinkingSignatures,
|
|
2214
2060
|
gitCheckpoint,
|
|
2215
2061
|
gitCheckpointAsync,
|
|
2216
2062
|
recordTokens,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function collectFilesInDir(fs, path, srcDir, opts = {}) {
|
|
4
|
+
const excludedScripts = opts.excludedScripts || new Set();
|
|
5
|
+
const applyExclusions = opts.applyExclusions === true;
|
|
6
|
+
const files = [];
|
|
7
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
8
|
+
const fullPath = path.join(srcDir, entry);
|
|
9
|
+
const stat = fs.statSync(fullPath);
|
|
10
|
+
if (!stat.isFile()) continue;
|
|
11
|
+
if (applyExclusions && excludedScripts.has(entry)) continue;
|
|
12
|
+
if (/\.test\.js$/.test(entry)) continue;
|
|
13
|
+
if (!/\.(js|yaml|sh)$/.test(entry)) continue;
|
|
14
|
+
files.push(entry);
|
|
15
|
+
}
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectNestedGroups(fs, path, rootDir, destPrefix = '') {
|
|
20
|
+
const groups = [];
|
|
21
|
+
const entries = fs.readdirSync(rootDir);
|
|
22
|
+
const files = [];
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = path.join(rootDir, entry);
|
|
26
|
+
const stat = fs.statSync(fullPath);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
const childPrefix = destPrefix ? path.join(destPrefix, entry) : entry;
|
|
29
|
+
groups.push(...collectNestedGroups(fs, path, fullPath, childPrefix));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (/\.test\.js$/.test(entry)) continue;
|
|
33
|
+
if (!/\.(js|yaml|sh)$/.test(entry)) continue;
|
|
34
|
+
files.push(entry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
groups.unshift({
|
|
38
|
+
srcDir: rootDir,
|
|
39
|
+
destSubdir: destPrefix,
|
|
40
|
+
fileList: files,
|
|
41
|
+
});
|
|
42
|
+
return groups;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectDeployGroups(fs, path, scriptsDir, opts = {}) {
|
|
46
|
+
const excludedScripts = opts.excludedScripts || new Set();
|
|
47
|
+
const includeNestedDirs = Array.isArray(opts.includeNestedDirs) ? opts.includeNestedDirs : [];
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
fs.statSync(scriptsDir);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const groups = [];
|
|
56
|
+
groups.push({
|
|
57
|
+
srcDir: scriptsDir,
|
|
58
|
+
destSubdir: '',
|
|
59
|
+
fileList: collectFilesInDir(fs, path, scriptsDir, { excludedScripts, applyExclusions: true }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
for (const dirName of includeNestedDirs) {
|
|
63
|
+
const srcDir = path.join(scriptsDir, dirName);
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) continue;
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
groups.push(...collectNestedGroups(fs, path, srcDir, dirName));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return groups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectSyntaxCheckFiles(path, deployGroups) {
|
|
76
|
+
const files = [];
|
|
77
|
+
for (const group of deployGroups || []) {
|
|
78
|
+
for (const file of group.fileList || []) {
|
|
79
|
+
if (!file.endsWith('.js')) continue;
|
|
80
|
+
files.push(path.join(group.srcDir, file));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
collectDeployGroups,
|
|
88
|
+
collectSyntaxCheckFiles,
|
|
89
|
+
collectNestedGroups,
|
|
90
|
+
};
|
|
@@ -43,18 +43,15 @@ feishu:
|
|
|
43
43
|
|
|
44
44
|
## 3. 会话与执行规则
|
|
45
45
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- 同一个底层 session 不做自动“恢复摘要”注入;续聊直接依赖引擎原生上下文
|
|
50
|
-
- 额外上下文只在显式链路进入时注入,例如 `/compact` 产物、`NOW.md`、memory facts / capsules、intent hints
|
|
46
|
+
- Runtime 工厂:`daemon-engine-runtime.js`
|
|
47
|
+
- 执行编排:`daemon-claude-engine.js`,streaming 纯逻辑委托 `core/handoff.js`(引擎中性),审计状态在 `core/audit.js`
|
|
48
|
+
- 架构纪律见 CLAUDE.md「代码架构纪律(Unix 哲学)」
|
|
51
49
|
|
|
52
50
|
### Codex 会话策略
|
|
53
51
|
|
|
54
52
|
- 首轮:`codex exec --json -`
|
|
55
53
|
- 续轮:`codex exec resume <thread_id> --json -`
|
|
56
54
|
- `resume` 失败自动重试:同一 `chatId` 在 10 分钟内最多 1 次
|
|
57
|
-
- 收到新 `thread_id` 时自动迁移 session id
|
|
58
55
|
|
|
59
56
|
## 4. 命令行为差异
|
|
60
57
|
|
|
@@ -152,6 +149,8 @@ feishu:
|
|
|
152
149
|
## 9. 双平台/双引擎维护矩阵
|
|
153
150
|
|
|
154
151
|
### 统一维护(改一处即可)
|
|
152
|
+
- **core/handoff.js**(引擎中性、平台中性的纯逻辑,通过参数接收平台/引擎差异)
|
|
153
|
+
- **core/audit.js**(纯状态管理,无平台差异)
|
|
155
154
|
- agent-layer.js / daemon-agent-tools.js / daemon-agent-commands.js / daemon-user-acl.js
|
|
156
155
|
- ENGINE_MODEL_CONFIG(daemon-engine-runtime.js 集中管理)
|
|
157
156
|
- daemon-runtime-lifecycle.js 的语法检查和备份机制
|
|
@@ -427,9 +426,13 @@ Event 类型:`MISSION_START` / `DISPATCH` / `MEMBER_COMPLETE` / `PHASE_GATE` /
|
|
|
427
426
|
|
|
428
427
|
## 14. 变更后维护动作
|
|
429
428
|
|
|
430
|
-
1.
|
|
431
|
-
|
|
432
|
-
|
|
429
|
+
1. 测试:
|
|
430
|
+
- `npm test`(全量)
|
|
431
|
+
- 改 `core/handoff.js` 时:`node --test scripts/core/handoff.test.js scripts/daemon-claude-engine.test.js`
|
|
432
|
+
- 改 `daemon.js` 审计相关时:`node --test scripts/daemon-audit.test.js`
|
|
433
|
+
2. Lint:`npx eslint scripts/daemon*.js scripts/core/*.js`
|
|
434
|
+
3. `npm run sync:plugin`
|
|
435
|
+
4. 更新文档:
|
|
433
436
|
- `scripts/docs/pointer-map.md`
|
|
434
|
-
- `
|
|
435
|
-
- `README中文版.md`
|
|
437
|
+
- `scripts/docs/maintenance-manual.md`
|
|
438
|
+
- `README.md` / `README中文版.md`
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- Daemon 主循环:`scripts/daemon.js`
|
|
10
10
|
- 多引擎 runtime 适配层:`scripts/daemon-engine-runtime.js`
|
|
11
11
|
- 会话执行引擎(Claude/Codex 共用入口):`scripts/daemon-claude-engine.js`
|
|
12
|
+
- **核心纯逻辑模块**:`scripts/core/handoff.js`(子进程生命周期)、`scripts/core/audit.js`(审计状态)
|
|
12
13
|
- 管理命令:`scripts/daemon-admin-commands.js`
|
|
13
14
|
- 命令路由:`scripts/daemon-command-router.js`
|
|
14
15
|
- 执行命令(`/stop`、`/compact` 等):`scripts/daemon-exec-commands.js`
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
- Provider/蒸馏模型配置:`scripts/providers.js`(`/provider`、`/distill-model`)
|
|
18
19
|
- 跨平台基础设施:`scripts/platform.js`(`killProcessTree`、`socketPath`、`sleepSync`、`icon`)
|
|
19
20
|
- 热重载安全机制:`scripts/daemon-runtime-lifecycle.js`(语法预检、last-good 备份、crash-loop 自愈)
|
|
21
|
+
- 打包工具:`scripts/deploy-manifest.js`(部署清单)、`scripts/sync-plugin.js`(plugin 镜像同步)
|
|
20
22
|
- 维护手册:`scripts/docs/maintenance-manual.md`
|
|
21
23
|
|
|
22
24
|
## 多引擎(Claude/Codex)定位
|
|
@@ -27,8 +29,7 @@
|
|
|
27
29
|
|
|
28
30
|
- 会话与引擎选择:
|
|
29
31
|
- `scripts/daemon-claude-engine.js`
|
|
30
|
-
- 关键点:`askClaude()`
|
|
31
|
-
- 说明:同一底层 session 续聊不再注入会话恢复摘要;额外上下文仅来自显式 compact / memory / intent 链路
|
|
32
|
+
- 关键点:`askClaude()` 路由+执行,streaming 纯逻辑委托 `core/handoff.js`;`patchSessionSerialized()` 串行回写避免竞态
|
|
32
33
|
- Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
|
|
33
34
|
|
|
34
35
|
- Agent Soul 身份层(新):
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
- 关键点:`ensureAgentLayer()` 创建 `~/.metame/agents/<id>/`(soul.md、memory-snapshot.md、agent.yaml);
|
|
37
38
|
`createLinkOrMirror()` Windows 兼容(symlink → hardlink → copy 降级);
|
|
38
39
|
`ensureClaudeMdSoulImport()` 在 CLAUDE.md 头部注入 `@SOUL.md`(Claude CLI 自动加载);
|
|
39
|
-
Codex 引擎在每次新 session 时将 CLAUDE.md + SOUL.md 合并写入 AGENTS.md
|
|
40
|
+
Codex 引擎在每次新 session 时将 CLAUDE.md + SOUL.md 合并写入 AGENTS.md;
|
|
40
41
|
`repairAgentLayer()` 懒迁移:老项目补建 soul 层,幂等安全
|
|
41
42
|
|
|
42
43
|
- Agent 命令处理(新):
|
|
@@ -62,6 +63,14 @@
|
|
|
62
63
|
- `scripts/daemon-admin-commands.js`
|
|
63
64
|
- 关键点:`/engine` 切换默认引擎;`/doctor` 按默认引擎检查 CLI 可用性(Claude/Codex)并兼容自定义 provider 模型名
|
|
64
65
|
|
|
66
|
+
## 核心模块层(scripts/core/)
|
|
67
|
+
|
|
68
|
+
纯逻辑,无副作用,返回意图标志由调用方执行。
|
|
69
|
+
|
|
70
|
+
- `core/handoff.js`:子进程 spawn/kill、streaming 状态机、超时看门狗、结果构建。唯一消费者 `daemon-claude-engine.js`
|
|
71
|
+
- `core/audit.js`:审计状态。唯一消费者 `daemon.js`
|
|
72
|
+
- 测试:`core/handoff.test.js`、`daemon-audit.test.js`、`daemon-claude-engine.test.js`
|
|
73
|
+
|
|
65
74
|
## 团队 Dispatch 与跨设备通信定位
|
|
66
75
|
|
|
67
76
|
- 共享 Dispatch 工具:
|
|
@@ -190,7 +199,7 @@
|
|
|
190
199
|
|
|
191
200
|
1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
|
|
192
201
|
2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
|
|
193
|
-
3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
|
|
202
|
+
3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/core/handoff.js`(纯逻辑)→ `scripts/mentor-engine.js`
|
|
194
203
|
4. 团队/跨设备:`scripts/daemon-team-dispatch.js` → `scripts/daemon-remote-dispatch.js` → `scripts/daemon-bridges.js`
|
|
195
204
|
5. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
|
|
196
205
|
|
|
@@ -112,15 +112,36 @@ function createBot(config) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// ── Thread-aware send primitive ──────────────────────────────────────
|
|
116
|
+
// Detects composite "thread:chatId:threadId" IDs from daemon-bridges
|
|
117
|
+
// and routes to client.im.message.reply (stays inside the topic thread)
|
|
118
|
+
// instead of client.im.message.create.
|
|
119
|
+
const { parseThreadChatId } = require('./core/thread-chat-id');
|
|
120
|
+
|
|
121
|
+
async function _dispatchSend(chatId, msgType, content, timeout = 15000) {
|
|
122
|
+
const thread = parseThreadChatId(chatId);
|
|
123
|
+
let res;
|
|
124
|
+
if (thread) {
|
|
125
|
+
// Topic mode: reply inside the thread so the response stays in the topic
|
|
126
|
+
res = await withTimeout(client.im.message.reply({
|
|
127
|
+
path: { message_id: thread.threadId },
|
|
128
|
+
data: { msg_type: msgType, content },
|
|
129
|
+
}), timeout);
|
|
130
|
+
} else {
|
|
131
|
+
// Normal mode: create message in chat
|
|
132
|
+
res = await withTimeout(client.im.message.create({
|
|
133
|
+
params: { receive_id_type: 'chat_id' },
|
|
134
|
+
data: { receive_id: chatId, msg_type: msgType, content },
|
|
135
|
+
}), timeout);
|
|
136
|
+
}
|
|
137
|
+
const msgId = res?.data?.message_id;
|
|
138
|
+
return msgId ? { message_id: msgId } : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
// Private: send an interactive card JSON; returns { message_id } or null.
|
|
116
142
|
// All card functions funnel through here to avoid repeating the SDK call.
|
|
117
143
|
async function _sendInteractive(chatId, card) {
|
|
118
|
-
|
|
119
|
-
params: { receive_id_type: 'chat_id' },
|
|
120
|
-
data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
|
|
121
|
-
}), 30000); // 30s: large card content can be slow; timeout must not fire after delivery
|
|
122
|
-
const msgId = res?.data?.message_id;
|
|
123
|
-
return msgId ? { message_id: msgId } : null;
|
|
144
|
+
return _dispatchSend(chatId, 'interactive', JSON.stringify(card), 30000);
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
let _editBroken = false; // closure var — safe against destructured calls
|
|
@@ -132,17 +153,7 @@ function createBot(config) {
|
|
|
132
153
|
* Send a plain text message
|
|
133
154
|
*/
|
|
134
155
|
async sendMessage(chatId, text) {
|
|
135
|
-
|
|
136
|
-
params: { receive_id_type: 'chat_id' },
|
|
137
|
-
data: {
|
|
138
|
-
receive_id: chatId,
|
|
139
|
-
msg_type: 'text',
|
|
140
|
-
content: JSON.stringify({ text }),
|
|
141
|
-
},
|
|
142
|
-
}));
|
|
143
|
-
// Return Telegram-compatible shape so daemon can edit it later
|
|
144
|
-
const msgId = res?.data?.message_id;
|
|
145
|
-
return msgId ? { message_id: msgId } : null;
|
|
156
|
+
return _dispatchSend(chatId, 'text', JSON.stringify({ text }));
|
|
146
157
|
},
|
|
147
158
|
|
|
148
159
|
async editMessage(chatId, messageId, text, header = null) {
|
|
@@ -345,16 +356,9 @@ function createBot(config) {
|
|
|
345
356
|
throw new Error(`No file_key in response: ${JSON.stringify(uploadRes)}`);
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
// 2. Send file message
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
data: {
|
|
352
|
-
receive_id: chatId,
|
|
353
|
-
msg_type: 'file',
|
|
354
|
-
content: JSON.stringify({ file_key: fileKey }),
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
const msgId = sendRes?.data?.message_id;
|
|
359
|
+
// 2. Send file message (thread-aware)
|
|
360
|
+
const sendResult = await _dispatchSend(chatId, 'file', JSON.stringify({ file_key: fileKey }));
|
|
361
|
+
const msgId = sendResult?.message_id;
|
|
358
362
|
if (caption) await this.sendMessage(chatId, caption);
|
|
359
363
|
return msgId ? { message_id: msgId } : null;
|
|
360
364
|
} catch (uploadErr) {
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
|
+
// Global safety net: hooks must NEVER crash or exit non-zero
|
|
23
|
+
process.on('uncaughtException', () => process.exit(0));
|
|
24
|
+
process.on('unhandledRejection', () => process.exit(0));
|
|
25
|
+
|
|
22
26
|
const fs = require('fs');
|
|
23
27
|
const path = require('path');
|
|
24
28
|
const os = require('os');
|
|
@@ -59,14 +63,13 @@ function run(data) {
|
|
|
59
63
|
let intentBlock = '';
|
|
60
64
|
try {
|
|
61
65
|
intentBlock = buildIntentHintBlock(prompt, config, projectKey);
|
|
62
|
-
} catch
|
|
63
|
-
process.stderr.write(`[intent-engine] registry: ${e.message}\n`);
|
|
66
|
+
} catch {
|
|
64
67
|
return exit();
|
|
65
68
|
}
|
|
66
69
|
if (!intentBlock) return exit();
|
|
67
70
|
|
|
68
71
|
process.stdout.write(JSON.stringify({
|
|
69
|
-
hookSpecificOutput: {
|
|
72
|
+
hookSpecificOutput: { additionalContext: intentBlock },
|
|
70
73
|
}));
|
|
71
74
|
exit();
|
|
72
75
|
}
|
|
@@ -31,5 +31,6 @@ module.exports = function detectMemoryRecall(prompt) {
|
|
|
31
31
|
'- 搜索记忆: `node ~/.metame/memory-search.js "关键词1" "keyword2"`',
|
|
32
32
|
'- 一次传 3-4 个关键词(中文+英文+函数名)',
|
|
33
33
|
'- `--facts` 只搜事实,`--sessions` 只搜会话',
|
|
34
|
+
'- 统一召回: `require("./memory").assembleContext({ query, scope: { project, agent } })`',
|
|
34
35
|
].join('\n');
|
|
35
36
|
};
|
|
@@ -70,7 +70,7 @@ const PERPETUAL_INTENTS = [
|
|
|
70
70
|
{
|
|
71
71
|
// User asks about event log or progress history
|
|
72
72
|
pattern: /(事件|event).{0,5}(日志|log)|progress\.tsv|进度日志/i,
|
|
73
|
-
hint: () => '[永续任务] 事件日志: `tail ~/.metame/
|
|
73
|
+
hint: () => '[永续任务] 事件日志: `tail ~/.metame/reactive/<project>/events.jsonl`\n进度表: `cat workspace/progress.tsv`',
|
|
74
74
|
},
|
|
75
75
|
];
|
|
76
76
|
|