metame-cli 1.5.0 → 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 +60 -132
- package/index.js +193 -77
- package/package.json +4 -3
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +80 -25
- package/scripts/daemon-agent-commands.js +81 -0
- package/scripts/daemon-agent-tools.js +145 -10
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +201 -169
- package/scripts/daemon-command-router.js +7 -2
- package/scripts/daemon-engine-runtime.js +74 -21
- package/scripts/daemon-exec-commands.js +5 -3
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +127 -4
- package/scripts/daemon-session-commands.js +23 -36
- package/scripts/daemon-session-store.js +120 -13
- package/scripts/daemon-task-scheduler.js +61 -11
- package/scripts/daemon-user-acl.js +10 -1
- package/scripts/daemon.js +192 -21
- package/scripts/distill.js +4 -0
- package/scripts/docs/maintenance-manual.md +39 -3
- package/scripts/docs/pointer-map.md +23 -1
- package/scripts/feishu-adapter.js +36 -12
- package/scripts/memory-extract.js +5 -1
- package/scripts/memory-nightly-reflect.js +3 -0
- package/scripts/mentor-engine.js +4 -4
- package/scripts/platform.js +22 -0
- package/scripts/providers.js +14 -2
- package/scripts/telegram-adapter.js +12 -8
package/scripts/daemon.js
CHANGED
|
@@ -35,13 +35,14 @@ process.on('uncaughtException', (err) => {
|
|
|
35
35
|
const fs = require('fs');
|
|
36
36
|
const path = require('path');
|
|
37
37
|
const os = require('os');
|
|
38
|
-
const { execSync, execFileSync, spawn } = require('child_process');
|
|
38
|
+
const { execSync, execFileSync, execFile, spawn } = require('child_process');
|
|
39
39
|
|
|
40
40
|
const HOME = os.homedir();
|
|
41
41
|
const METAME_DIR = path.join(HOME, '.metame');
|
|
42
42
|
const CONFIG_FILE = path.join(METAME_DIR, 'daemon.yaml');
|
|
43
43
|
const STATE_FILE = path.join(METAME_DIR, 'daemon_state.json');
|
|
44
44
|
const PID_FILE = path.join(METAME_DIR, 'daemon.pid');
|
|
45
|
+
const LOCK_FILE = path.join(METAME_DIR, 'daemon.lock');
|
|
45
46
|
const LOG_FILE = path.join(METAME_DIR, 'daemon.log');
|
|
46
47
|
const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
47
48
|
const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
|
|
@@ -59,7 +60,7 @@ const CLAUDE_BIN = (() => {
|
|
|
59
60
|
];
|
|
60
61
|
try {
|
|
61
62
|
const cmd = process.platform === 'win32' ? 'where claude' : 'which claude 2>/dev/null';
|
|
62
|
-
return execSync(cmd, { encoding: 'utf8' }).trim().split('\n')[0];
|
|
63
|
+
return execSync(cmd, { encoding: 'utf8', ...(process.platform === 'win32' ? { windowsHide: true } : {}) }).trim().split('\n')[0];
|
|
63
64
|
} catch {}
|
|
64
65
|
for (const p of candidates) { if (fs.existsSync(p)) return p; }
|
|
65
66
|
return 'claude'; // fallback: hope it's in PATH
|
|
@@ -144,7 +145,7 @@ const { createFileBrowser } = require('./daemon-file-browser');
|
|
|
144
145
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
145
146
|
const { createNotifier } = require('./daemon-notify');
|
|
146
147
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
147
|
-
const { createEngineRuntimeFactory, detectDefaultEngine, ENGINE_DISTILL_MAP } = require('./daemon-engine-runtime');
|
|
148
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
|
|
148
149
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
149
150
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
150
151
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -164,6 +165,11 @@ function getDaemonProviderEnv() {
|
|
|
164
165
|
try { return providerMod.buildDaemonEnv(); } catch { return {}; }
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
function getDistillModel() {
|
|
169
|
+
if (!providerMod || typeof providerMod.getDistillModel !== 'function') return 'haiku';
|
|
170
|
+
try { return providerMod.getDistillModel(); } catch { return 'haiku'; }
|
|
171
|
+
}
|
|
172
|
+
|
|
167
173
|
function getActiveProviderEnv() {
|
|
168
174
|
if (!providerMod) return {};
|
|
169
175
|
try { return providerMod.buildActiveEnv(); } catch { return {}; }
|
|
@@ -206,9 +212,10 @@ const {
|
|
|
206
212
|
cpExtractTimestamp,
|
|
207
213
|
cpDisplayLabel,
|
|
208
214
|
gitCheckpoint,
|
|
215
|
+
gitCheckpointAsync,
|
|
209
216
|
listCheckpoints,
|
|
210
217
|
cleanupCheckpoints,
|
|
211
|
-
} = createCheckpointUtils({ execSync, path, log });
|
|
218
|
+
} = createCheckpointUtils({ execSync, execFile, path, log });
|
|
212
219
|
|
|
213
220
|
// ---------------------------------------------------------
|
|
214
221
|
// CONFIG & STATE
|
|
@@ -894,6 +901,8 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
894
901
|
* Spawn session-summarize.js for sessions that have been idle 2-24 hours.
|
|
895
902
|
* Called on sleep mode entry. Skips sessions that already have a fresh summary.
|
|
896
903
|
*/
|
|
904
|
+
const MAX_CONCURRENT_SUMMARIES = 3;
|
|
905
|
+
|
|
897
906
|
function spawnSessionSummaries() {
|
|
898
907
|
const scriptPath = path.join(__dirname, 'session-summarize.js');
|
|
899
908
|
if (!fs.existsSync(scriptPath)) return;
|
|
@@ -901,18 +910,43 @@ function spawnSessionSummaries() {
|
|
|
901
910
|
const now = Date.now();
|
|
902
911
|
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
903
912
|
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
913
|
+
// Collect eligible sessions, sort by most recently active first
|
|
914
|
+
const eligible = [];
|
|
904
915
|
for (const [cid, sess] of Object.entries(state.sessions || {})) {
|
|
905
|
-
|
|
916
|
+
// Support both old flat format and new per-engine format
|
|
917
|
+
let sessionId, started;
|
|
918
|
+
if (sess.engines) {
|
|
919
|
+
const active = Object.values(sess.engines).find(s => s.id && s.started);
|
|
920
|
+
if (!active) continue;
|
|
921
|
+
sessionId = active.id;
|
|
922
|
+
started = true;
|
|
923
|
+
} else {
|
|
924
|
+
sessionId = sess.id;
|
|
925
|
+
started = sess.started;
|
|
926
|
+
}
|
|
927
|
+
if (!sessionId || !started) continue;
|
|
906
928
|
const lastActive = sess.last_active || 0;
|
|
907
929
|
const idleMs = now - lastActive;
|
|
908
930
|
if (idleMs < TWO_HOURS || idleMs > SEVEN_DAYS) continue;
|
|
909
|
-
// Skip if summary is already newer than last activity
|
|
910
931
|
if ((sess.last_summary_at || 0) > lastActive) continue;
|
|
932
|
+
eligible.push({ cid, sess: { ...sess, id: sessionId, started }, lastActive });
|
|
933
|
+
}
|
|
934
|
+
eligible.sort((a, b) => b.lastActive - a.lastActive);
|
|
935
|
+
|
|
936
|
+
let spawned = 0;
|
|
937
|
+
for (const { cid, sess } of eligible) {
|
|
938
|
+
if (spawned >= MAX_CONCURRENT_SUMMARIES) {
|
|
939
|
+
log('INFO', `[DAEMON] Session summary concurrency limit (${MAX_CONCURRENT_SUMMARIES}) reached, deferring remaining`);
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
const idleMs = now - (sess.last_active || 0);
|
|
911
943
|
try {
|
|
912
944
|
const child = spawn(process.execPath, [scriptPath, cid, sess.id], {
|
|
913
945
|
detached: true, stdio: 'ignore',
|
|
946
|
+
...(process.platform === 'win32' ? { windowsHide: true } : {}),
|
|
914
947
|
});
|
|
915
948
|
child.unref();
|
|
949
|
+
spawned++;
|
|
916
950
|
log('INFO', `[DAEMON] Session summary spawned for ${cid} (idle ${Math.round(idleMs / 3600000)}h)`);
|
|
917
951
|
} catch (e) {
|
|
918
952
|
log('WARN', `[DAEMON] Failed to spawn session summary: ${e.message}`);
|
|
@@ -1253,11 +1287,13 @@ const {
|
|
|
1253
1287
|
buildSessionCardElements,
|
|
1254
1288
|
listProjectDirs,
|
|
1255
1289
|
getSession,
|
|
1290
|
+
getSessionForEngine,
|
|
1256
1291
|
createSession,
|
|
1257
1292
|
getSessionName,
|
|
1258
1293
|
writeSessionName,
|
|
1259
1294
|
markSessionStarted,
|
|
1260
1295
|
watchSessionFiles,
|
|
1296
|
+
isEngineSessionValid,
|
|
1261
1297
|
} = createSessionStore({
|
|
1262
1298
|
fs,
|
|
1263
1299
|
path,
|
|
@@ -1361,6 +1397,16 @@ if (providerMod && typeof providerMod.setEngine === 'function') {
|
|
|
1361
1397
|
}
|
|
1362
1398
|
log('INFO', `Default engine: ${_defaultEngine} (detected: ${detectedEngine})`);
|
|
1363
1399
|
|
|
1400
|
+
// One-time migration: daemon.model (legacy) → daemon.models.<engine>
|
|
1401
|
+
try {
|
|
1402
|
+
const _migCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
1403
|
+
if (_migCfg.daemon && _migCfg.daemon.model && !_migCfg.daemon.models) {
|
|
1404
|
+
_migCfg.daemon.models = { [_defaultEngine]: _migCfg.daemon.model };
|
|
1405
|
+
writeConfigSafe(_migCfg);
|
|
1406
|
+
log('INFO', `Migrated daemon.model="${_migCfg.daemon.model}" → daemon.models.${_defaultEngine}`);
|
|
1407
|
+
}
|
|
1408
|
+
} catch { /* ignore */ }
|
|
1409
|
+
|
|
1364
1410
|
function getDefaultEngine() {
|
|
1365
1411
|
return _defaultEngine;
|
|
1366
1412
|
}
|
|
@@ -1371,16 +1417,24 @@ function setDefaultEngine(engine) {
|
|
|
1371
1417
|
st.default_engine = engine;
|
|
1372
1418
|
saveState(st);
|
|
1373
1419
|
if (providerMod) {
|
|
1374
|
-
//
|
|
1420
|
+
// Sync distill model to this engine's default
|
|
1375
1421
|
if (typeof providerMod.setDistillModel === 'function') {
|
|
1376
|
-
const
|
|
1377
|
-
try { providerMod.setDistillModel(
|
|
1422
|
+
const distill = (ENGINE_MODEL_CONFIG[engine] || ENGINE_MODEL_CONFIG.claude).distill;
|
|
1423
|
+
try { providerMod.setDistillModel(distill); } catch { /* ignore */ }
|
|
1378
1424
|
}
|
|
1379
|
-
// Couple distill binary
|
|
1380
1425
|
if (typeof providerMod.setEngine === 'function') {
|
|
1381
1426
|
try { providerMod.setEngine(engine); } catch { /* ignore */ }
|
|
1382
1427
|
}
|
|
1383
1428
|
}
|
|
1429
|
+
// Migrate old daemon.model → daemon.models[engine] on first switch
|
|
1430
|
+
try {
|
|
1431
|
+
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
1432
|
+
if (!cfg.daemon) cfg.daemon = {};
|
|
1433
|
+
if (cfg.daemon.model && !cfg.daemon.models) {
|
|
1434
|
+
cfg.daemon.models = { [engine]: cfg.daemon.model };
|
|
1435
|
+
writeConfigSafe(cfg);
|
|
1436
|
+
}
|
|
1437
|
+
} catch { /* ignore */ }
|
|
1384
1438
|
}
|
|
1385
1439
|
|
|
1386
1440
|
const getEngineRuntime = createEngineRuntimeFactory({
|
|
@@ -1413,6 +1467,7 @@ const {
|
|
|
1413
1467
|
recordTokens,
|
|
1414
1468
|
buildProfilePreamble,
|
|
1415
1469
|
getDaemonProviderEnv,
|
|
1470
|
+
getDistillModel,
|
|
1416
1471
|
log,
|
|
1417
1472
|
physiologicalHeartbeat,
|
|
1418
1473
|
isUserIdle,
|
|
@@ -1460,6 +1515,7 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1460
1515
|
saveState,
|
|
1461
1516
|
getDefaultEngine,
|
|
1462
1517
|
setDefaultEngine,
|
|
1518
|
+
getDistillModel,
|
|
1463
1519
|
});
|
|
1464
1520
|
|
|
1465
1521
|
const { handleSessionCommand } = createSessionCommandHandler({
|
|
@@ -1519,12 +1575,15 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
1519
1575
|
sendFileButtons,
|
|
1520
1576
|
findSessionFile,
|
|
1521
1577
|
listRecentSessions,
|
|
1578
|
+
isEngineSessionValid,
|
|
1522
1579
|
getSession,
|
|
1580
|
+
getSessionForEngine,
|
|
1523
1581
|
createSession,
|
|
1524
1582
|
getSessionName,
|
|
1525
1583
|
writeSessionName,
|
|
1526
1584
|
markSessionStarted,
|
|
1527
1585
|
gitCheckpoint,
|
|
1586
|
+
gitCheckpointAsync,
|
|
1528
1587
|
recordTokens,
|
|
1529
1588
|
skillEvolution,
|
|
1530
1589
|
touchInteraction,
|
|
@@ -1617,6 +1676,7 @@ const { handleExecCommand } = createExecCommandHandler({
|
|
|
1617
1676
|
createSession,
|
|
1618
1677
|
findSessionFile,
|
|
1619
1678
|
loadConfig,
|
|
1679
|
+
getDistillModel,
|
|
1620
1680
|
});
|
|
1621
1681
|
|
|
1622
1682
|
const { handleOpsCommand } = createOpsCommandHandler({
|
|
@@ -1705,6 +1765,72 @@ function sleep(ms) {
|
|
|
1705
1765
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1706
1766
|
}
|
|
1707
1767
|
|
|
1768
|
+
let daemonLockFd = null;
|
|
1769
|
+
function isPidAlive(pid) {
|
|
1770
|
+
if (!pid || Number.isNaN(pid)) return false;
|
|
1771
|
+
try {
|
|
1772
|
+
process.kill(pid, 0);
|
|
1773
|
+
return true;
|
|
1774
|
+
} catch {
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function acquireDaemonLock() {
|
|
1780
|
+
const restartFromPid = parseInt(process.env.METAME_RESTART_FROM_PID || '', 10);
|
|
1781
|
+
const maxAttempts = restartFromPid ? 6 : 2;
|
|
1782
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1783
|
+
try {
|
|
1784
|
+
daemonLockFd = fs.openSync(LOCK_FILE, 'wx');
|
|
1785
|
+
fs.writeFileSync(daemonLockFd, JSON.stringify({
|
|
1786
|
+
pid: process.pid,
|
|
1787
|
+
started_at: new Date().toISOString(),
|
|
1788
|
+
}), 'utf8');
|
|
1789
|
+
return true;
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
if (e.code !== 'EEXIST') {
|
|
1792
|
+
log('ERROR', `Failed to acquire daemon lock: ${e.message}`);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
try {
|
|
1796
|
+
const raw = fs.readFileSync(LOCK_FILE, 'utf8');
|
|
1797
|
+
const meta = JSON.parse(raw || '{}');
|
|
1798
|
+
const ownerPid = parseInt(meta.pid, 10);
|
|
1799
|
+
if (isPidAlive(ownerPid)) {
|
|
1800
|
+
// Restart handoff: allow child to wait for parent to exit and take over.
|
|
1801
|
+
if (restartFromPid && ownerPid === restartFromPid) {
|
|
1802
|
+
for (let i = 0; i < 30; i++) {
|
|
1803
|
+
sleepSync(500);
|
|
1804
|
+
if (!isPidAlive(ownerPid)) break;
|
|
1805
|
+
}
|
|
1806
|
+
if (isPidAlive(ownerPid)) {
|
|
1807
|
+
log('WARN', `Restart handoff timed out, previous daemon still alive (PID: ${ownerPid})`);
|
|
1808
|
+
if (attempt < maxAttempts - 1) continue;
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
log('WARN', `Another daemon instance owns lock (PID: ${meta.pid})`);
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
} catch {
|
|
1818
|
+
// Ignore malformed lock metadata and treat as stale.
|
|
1819
|
+
}
|
|
1820
|
+
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function releaseDaemonLock() {
|
|
1827
|
+
try {
|
|
1828
|
+
if (daemonLockFd !== null) fs.closeSync(daemonLockFd);
|
|
1829
|
+
} catch { /* ignore */ }
|
|
1830
|
+
daemonLockFd = null;
|
|
1831
|
+
try { if (fs.existsSync(LOCK_FILE)) fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1708
1834
|
// ---------------------------------------------------------
|
|
1709
1835
|
// MAIN
|
|
1710
1836
|
// ---------------------------------------------------------
|
|
@@ -1724,7 +1850,9 @@ async function main() {
|
|
|
1724
1850
|
// Config validation: warn on unknown/suspect fields
|
|
1725
1851
|
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
1726
1852
|
const KNOWN_DAEMON = [
|
|
1727
|
-
'model',
|
|
1853
|
+
'model', // legacy (still valid as fallback)
|
|
1854
|
+
'models', // per-engine model map: { claude, codex }
|
|
1855
|
+
'distill_models', // per-engine distill model map
|
|
1728
1856
|
'log_max_size',
|
|
1729
1857
|
'heartbeat_check_interval',
|
|
1730
1858
|
'session_allowed_tools',
|
|
@@ -1736,7 +1864,8 @@ async function main() {
|
|
|
1736
1864
|
'enable_nl_mac_control',
|
|
1737
1865
|
'enable_nl_mac_fallback',
|
|
1738
1866
|
];
|
|
1739
|
-
|
|
1867
|
+
// All known models across all engines (for legacy daemon.model validation only)
|
|
1868
|
+
const BUILTIN_CLAUDE_MODELS = ENGINE_MODEL_CONFIG.claude.options;
|
|
1740
1869
|
for (const key of Object.keys(config)) {
|
|
1741
1870
|
if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
|
|
1742
1871
|
}
|
|
@@ -1744,17 +1873,22 @@ async function main() {
|
|
|
1744
1873
|
for (const key of Object.keys(config.daemon)) {
|
|
1745
1874
|
if (!KNOWN_DAEMON.includes(key)) log('WARN', `Config: unknown daemon.${key} (typo?)`);
|
|
1746
1875
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1876
|
+
// Validate legacy daemon.model (only warn if anthropic provider + unknown Claude model)
|
|
1877
|
+
if (config.daemon.model && !BUILTIN_CLAUDE_MODELS.includes(config.daemon.model)) {
|
|
1749
1878
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
1750
|
-
if (activeProv === 'anthropic') {
|
|
1751
|
-
log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known model`);
|
|
1879
|
+
if (activeProv === 'anthropic' && _defaultEngine === 'claude') {
|
|
1880
|
+
log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known Claude model`);
|
|
1752
1881
|
} else {
|
|
1753
|
-
log('INFO', `Config:
|
|
1882
|
+
log('INFO', `Config: model "${config.daemon.model}" for engine "${_defaultEngine}" / provider "${activeProv}"`);
|
|
1754
1883
|
}
|
|
1755
1884
|
}
|
|
1756
1885
|
}
|
|
1757
1886
|
|
|
1887
|
+
if (!acquireDaemonLock()) {
|
|
1888
|
+
process.exit(0);
|
|
1889
|
+
}
|
|
1890
|
+
process.on('exit', releaseDaemonLock);
|
|
1891
|
+
|
|
1758
1892
|
// Takeover: kill any existing daemon
|
|
1759
1893
|
killExistingDaemon();
|
|
1760
1894
|
writePid();
|
|
@@ -1819,6 +1953,32 @@ async function main() {
|
|
|
1819
1953
|
// Start heartbeat scheduler
|
|
1820
1954
|
let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
|
|
1821
1955
|
|
|
1956
|
+
let shuttingDown = false;
|
|
1957
|
+
function spawnReplacementDaemon(reason) {
|
|
1958
|
+
try {
|
|
1959
|
+
const replacementScript = path.join(METAME_DIR, 'daemon.js');
|
|
1960
|
+
const bg = spawn(process.execPath, [replacementScript], {
|
|
1961
|
+
detached: process.platform !== 'win32',
|
|
1962
|
+
stdio: 'ignore',
|
|
1963
|
+
windowsHide: true,
|
|
1964
|
+
cwd: METAME_DIR,
|
|
1965
|
+
env: {
|
|
1966
|
+
...process.env,
|
|
1967
|
+
HOME,
|
|
1968
|
+
USERPROFILE: HOME,
|
|
1969
|
+
METAME_ROOT: process.env.METAME_ROOT || path.dirname(__filename),
|
|
1970
|
+
METAME_RESTART_FROM_PID: String(process.pid),
|
|
1971
|
+
},
|
|
1972
|
+
});
|
|
1973
|
+
bg.unref();
|
|
1974
|
+
log('INFO', `[RESTART] Spawned replacement daemon (PID: ${bg.pid}) reason=${reason}`);
|
|
1975
|
+
return true;
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
log('ERROR', `[RESTART] Failed to spawn replacement daemon: ${e.message}`);
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1822
1982
|
const runtimeWatchers = setupRuntimeWatchers({
|
|
1823
1983
|
fs,
|
|
1824
1984
|
path,
|
|
@@ -1839,8 +1999,8 @@ async function main() {
|
|
|
1839
1999
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
1840
2000
|
setHeartbeatTimer: (next) => { heartbeatTimer = next; },
|
|
1841
2001
|
onRestartRequested: () => {
|
|
1842
|
-
// Reuse full shutdown logic
|
|
1843
|
-
shutdown().catch(() => process.exit(
|
|
2002
|
+
// Reuse full shutdown logic, then self-spawn replacement.
|
|
2003
|
+
shutdown({ restartReason: 'daemon-script-changed' }).catch(() => process.exit(1));
|
|
1844
2004
|
},
|
|
1845
2005
|
});
|
|
1846
2006
|
// Expose reloadConfig to handleCommand via closure
|
|
@@ -1872,7 +2032,17 @@ async function main() {
|
|
|
1872
2032
|
}
|
|
1873
2033
|
|
|
1874
2034
|
// Graceful shutdown
|
|
1875
|
-
const shutdown = async () => {
|
|
2035
|
+
const shutdown = async (opts = {}) => {
|
|
2036
|
+
if (shuttingDown) return;
|
|
2037
|
+
shuttingDown = true; // set immediately to prevent double-spawn race condition
|
|
2038
|
+
if (opts.restartReason) {
|
|
2039
|
+
const spawned = spawnReplacementDaemon(opts.restartReason);
|
|
2040
|
+
if (!spawned) {
|
|
2041
|
+
log('ERROR', `[RESTART] Abort shutdown: failed to spawn replacement (${opts.restartReason})`);
|
|
2042
|
+
shuttingDown = false;
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1876
2046
|
log('INFO', 'Daemon shutting down...');
|
|
1877
2047
|
await notifyActiveUsers('关闭').catch(() => {});
|
|
1878
2048
|
runtimeWatchers.stop();
|
|
@@ -1891,6 +2061,7 @@ async function main() {
|
|
|
1891
2061
|
activeProcesses.clear();
|
|
1892
2062
|
try { if (fs.existsSync(ACTIVE_PIDS_FILE)) fs.unlinkSync(ACTIVE_PIDS_FILE); } catch { }
|
|
1893
2063
|
cleanPid();
|
|
2064
|
+
releaseDaemonLock();
|
|
1894
2065
|
const s = loadState();
|
|
1895
2066
|
s.pid = null;
|
|
1896
2067
|
saveState(s);
|
|
@@ -1914,7 +2085,7 @@ async function main() {
|
|
|
1914
2085
|
st.watchdog_restart = new Date().toISOString();
|
|
1915
2086
|
st.watchdog_stall_seconds = Math.round(elapsed / 1000);
|
|
1916
2087
|
saveState(st);
|
|
1917
|
-
process.exit(1);
|
|
2088
|
+
shutdown({ restartReason: 'watchdog-stall' }).catch(() => process.exit(1));
|
|
1918
2089
|
}
|
|
1919
2090
|
} catch (e) {
|
|
1920
2091
|
log('WARN', `[WATCHDOG] Check failed: ${e.message}`);
|
package/scripts/distill.js
CHANGED
|
@@ -1409,5 +1409,9 @@ if (require.main === module) {
|
|
|
1409
1409
|
} else {
|
|
1410
1410
|
console.log(`💤 ${result.summary}`);
|
|
1411
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}`);
|
|
1412
1416
|
})();
|
|
1413
1417
|
}
|
|
@@ -72,13 +72,30 @@ feishu:
|
|
|
72
72
|
- 仅在“当前默认引擎对应 CLI 不可用”时判为故障
|
|
73
73
|
- 自定义 provider 下允许任意合法模型名(不再强制 sonnet/opus/haiku)
|
|
74
74
|
|
|
75
|
-
## 5.
|
|
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. 运行时文件与状态
|
|
76
85
|
|
|
77
86
|
- 配置:`~/.metame/daemon.yaml`
|
|
78
87
|
- daemon 状态:`~/.metame/daemon_state.json`
|
|
79
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
|
|
80
97
|
|
|
81
|
-
##
|
|
98
|
+
## 8. 常见故障排查
|
|
82
99
|
|
|
83
100
|
### Codex 认证失败
|
|
84
101
|
|
|
@@ -109,7 +126,26 @@ feishu:
|
|
|
109
126
|
2. 若仍失败,手动 `/new` 新开会话
|
|
110
127
|
3. 检查 `~/.metame/active_agent_pids.json` 是否残留异常进程
|
|
111
128
|
|
|
112
|
-
##
|
|
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. 变更后维护动作
|
|
113
149
|
|
|
114
150
|
1. `npm test`
|
|
115
151
|
2. `npm run sync:plugin`
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
- 会话存储:`scripts/daemon-session-store.js`
|
|
16
16
|
- 默认配置:`scripts/daemon-default.yaml`
|
|
17
17
|
- Provider/蒸馏模型配置:`scripts/providers.js`(`/provider`、`/distill-model`)
|
|
18
|
+
- 跨平台基础设施:`scripts/platform.js`(`killProcessTree`、`socketPath`、`sleepSync`、`icon`)
|
|
19
|
+
- 热重载安全机制:`scripts/daemon-runtime-lifecycle.js`(语法预检、last-good 备份、crash-loop 自愈)
|
|
18
20
|
- 维护手册:`scripts/docs/maintenance-manual.md`
|
|
19
21
|
|
|
20
22
|
## 多引擎(Claude/Codex)定位
|
|
@@ -28,10 +30,24 @@
|
|
|
28
30
|
- 关键点:`askClaude()` 按 `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
|
|
29
31
|
- Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
|
|
30
32
|
|
|
33
|
+
- Agent Soul 身份层(新):
|
|
34
|
+
- `scripts/agent-layer.js`
|
|
35
|
+
- 关键点:`ensureAgentLayer()` 创建 `~/.metame/agents/<id>/`(soul.md、memory-snapshot.md、agent.yaml);
|
|
36
|
+
`createLinkOrMirror()` Windows 兼容(symlink → hardlink → copy 降级);
|
|
37
|
+
`ensureClaudeMdSoulImport()` 在 CLAUDE.md 头部注入 `@SOUL.md`(Claude CLI 自动加载);
|
|
38
|
+
Codex 引擎在每次新 session 时将 CLAUDE.md + SOUL.md 合并写入 AGENTS.md(见 daemon-claude-engine.js:957);
|
|
39
|
+
`repairAgentLayer()` 懒迁移:老项目补建 soul 层,幂等安全
|
|
40
|
+
|
|
41
|
+
- Agent 命令处理(新):
|
|
42
|
+
- `scripts/daemon-agent-commands.js`
|
|
43
|
+
- 关键点:`createAgentCommandHandler()` 处理 `/agent`、`/activate`、`/resume`;
|
|
44
|
+
`/agent soul [repair|edit]`;`pendingActivations` 无 TTL(消费即删);防止创建群自激活
|
|
45
|
+
|
|
31
46
|
- 路由与 Agent 创建:
|
|
32
47
|
- `scripts/daemon-command-router.js`
|
|
33
48
|
- `scripts/daemon-agent-tools.js`
|
|
34
|
-
- 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex
|
|
49
|
+
- 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`;
|
|
50
|
+
`bindAgentToChat()` 自动调用 `ensureAgentMetadata()` 建立 soul 层
|
|
35
51
|
|
|
36
52
|
- 会话命令与兼容边界:
|
|
37
53
|
- `scripts/daemon-exec-commands.js`
|
|
@@ -74,6 +90,12 @@
|
|
|
74
90
|
- 夜间反思文档:`~/.metame/memory/decisions/`、`~/.metame/memory/lessons/`
|
|
75
91
|
- 知识胶囊:`~/.metame/memory/capsules/`
|
|
76
92
|
- 复盘文档:`~/.metame/memory/postmortems/`
|
|
93
|
+
- **Agent Soul 层**:`~/.metame/agents/<agent_id>/`
|
|
94
|
+
- `agent.yaml` — id / name / engine / aliases
|
|
95
|
+
- `soul.md` — 身份定义(主文件,项目目录的 SOUL.md 是其链接)
|
|
96
|
+
- `memory-snapshot.md` — 近期记忆快照(注入 session prompt)
|
|
97
|
+
- 项目视图:`<cwd>/SOUL.md`(symlink/hardlink/copy)、`<cwd>/MEMORY.md`(同)
|
|
98
|
+
- `<cwd>/AGENTS.md` — Codex 专用,每次新 session 由 daemon 合并 CLAUDE.md + SOUL.md 写入
|
|
77
99
|
|
|
78
100
|
## 诊断顺序(推荐)
|
|
79
101
|
|
|
@@ -10,15 +10,40 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
12
|
let Lark;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} catch {
|
|
13
|
+
function _tryRequireLark() {
|
|
14
|
+
// 1. local node_modules (dev environment)
|
|
15
|
+
try { return require('@larksuiteoapi/node-sdk'); } catch {}
|
|
16
|
+
// 2. METAME_ROOT/node_modules (packaged metame-cli)
|
|
16
17
|
const metameRoot = process.env.METAME_ROOT;
|
|
17
18
|
if (metameRoot) {
|
|
18
|
-
|
|
19
|
+
try { return require(path.join(metameRoot, 'node_modules', '@larksuiteoapi/node-sdk')); } catch {}
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// 3. ~/.metame/node_modules (auto-installed for new users)
|
|
22
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
23
|
+
if (home) {
|
|
24
|
+
try { return require(path.join(home, '.metame', 'node_modules', '@larksuiteoapi', 'node-sdk')); } catch {}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
Lark = _tryRequireLark();
|
|
29
|
+
if (!Lark) {
|
|
30
|
+
// Auto-install into ~/.metame so new users never see this error
|
|
31
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
32
|
+
const prefix = home ? path.join(home, '.metame') : null;
|
|
33
|
+
if (prefix) {
|
|
34
|
+
console.log('[feishu] @larksuiteoapi/node-sdk not found, auto-installing into ~/.metame ...');
|
|
35
|
+
const { execSync } = require('child_process');
|
|
36
|
+
try {
|
|
37
|
+
execSync(`npm install @larksuiteoapi/node-sdk --prefix "${prefix}" --silent`, { stdio: 'inherit' });
|
|
38
|
+
Lark = require(path.join(prefix, 'node_modules', '@larksuiteoapi', 'node-sdk'));
|
|
39
|
+
console.log('[feishu] SDK installed successfully.');
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[feishu] Auto-install failed:', e.message);
|
|
42
|
+
console.error('Manual fix: npm install @larksuiteoapi/node-sdk --prefix ~/.metame');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
console.error('[feishu] Cannot find @larksuiteoapi/node-sdk and HOME is not set.');
|
|
22
47
|
process.exit(1);
|
|
23
48
|
}
|
|
24
49
|
}
|
|
@@ -26,12 +51,11 @@ try {
|
|
|
26
51
|
// Timeout wrapper: prevents SDK calls from hanging indefinitely when
|
|
27
52
|
// Feishu's token refresh HTTP request has no response (e.g. network down)
|
|
28
53
|
function withTimeout(promise, ms = 10000) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
]);
|
|
54
|
+
let timer;
|
|
55
|
+
const timeout = new Promise((_, reject) => {
|
|
56
|
+
timer = setTimeout(() => reject(new Error(`Feishu API timeout after ${ms}ms`)), ms);
|
|
57
|
+
});
|
|
58
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
// Max chars per lark_md element (Feishu limit ~4000)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* into memory.db. Runs independently of raw_signals.jsonl so that
|
|
8
8
|
* pure technical sessions (no preference signals) are still captured.
|
|
9
9
|
*
|
|
10
|
-
* Designed to run as a standalone heartbeat task
|
|
10
|
+
* Designed to run as a standalone heartbeat task (default interval: 4h).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
'use strict';
|
|
@@ -356,6 +356,10 @@ async function run() {
|
|
|
356
356
|
if (require.main === module) {
|
|
357
357
|
run().then(({ sessionsProcessed, factsSaved, factsSkipped }) => {
|
|
358
358
|
console.log(`✅ memory-extract: ${sessionsProcessed} session(s), ${factsSaved} facts saved, ${factsSkipped} skipped`);
|
|
359
|
+
// Report estimated token usage for daemon budget tracking
|
|
360
|
+
// Each session processed ≈ 1 callHaiku invocation ≈ 3k tokens
|
|
361
|
+
const estTokens = sessionsProcessed * 3000;
|
|
362
|
+
if (estTokens > 0) console.log(`__TOKENS__:${estTokens}`);
|
|
359
363
|
}).catch(e => {
|
|
360
364
|
console.error(`[memory-extract] Fatal: ${e.message}`);
|
|
361
365
|
process.exit(1);
|
|
@@ -465,6 +465,9 @@ facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0, 5000)}
|
|
|
465
465
|
if (require.main === module) {
|
|
466
466
|
run().then(() => {
|
|
467
467
|
console.log('✅ nightly-reflect complete');
|
|
468
|
+
// Report estimated token usage for daemon budget tracking
|
|
469
|
+
// ~5k tokens per reflection + capsule generation
|
|
470
|
+
console.log('__TOKENS__:5000');
|
|
468
471
|
}).catch(e => {
|
|
469
472
|
console.error(`[NIGHTLY-REFLECT] Fatal: ${e.message}`);
|
|
470
473
|
process.exit(1);
|
package/scripts/mentor-engine.js
CHANGED
|
@@ -365,14 +365,14 @@ function buildMentorPrompt(sessionState = {}, profile = {}, config = {}, nowMs =
|
|
|
365
365
|
lines.push(`- mode=${mode}, zone=${zone}`);
|
|
366
366
|
|
|
367
367
|
if (mode === 'gentle') {
|
|
368
|
-
lines.push('-
|
|
368
|
+
lines.push('- Give solution but include brief rationale so user can learn the "why".');
|
|
369
369
|
} else if (mode === 'active') {
|
|
370
|
-
lines.push('-
|
|
371
|
-
lines.push('-
|
|
370
|
+
lines.push('- Lead with the key concept/principle before the implementation.');
|
|
371
|
+
lines.push('- Add one-line "关键收获" at the end of your reply.');
|
|
372
372
|
} else {
|
|
373
373
|
lines.push('- Prefer scaffold/pseudocode first; avoid dumping full solution immediately.');
|
|
374
|
-
lines.push('- Use Socratic prompts to force active reasoning.');
|
|
375
374
|
lines.push('- Apply knowledge firewall: do not fill user logic gaps with unstated assumptions.');
|
|
375
|
+
lines.push('- Guide via explanation structure, not by asking the user questions.');
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
if (zone === 'comfort') lines.push('- Increase challenge slightly (new method or stronger abstraction).');
|