metame-cli 1.5.8 → 1.5.9
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/package.json +1 -1
- package/scripts/daemon-bridges.js +36 -44
- package/scripts/daemon-checkpoints.js +38 -24
- package/scripts/daemon-claude-engine.js +238 -58
- package/scripts/daemon-command-router.js +6 -125
- package/scripts/daemon-command-session-route.js +7 -1
- package/scripts/daemon-engine-runtime.js +8 -1
- package/scripts/daemon-exec-commands.js +36 -25
- package/scripts/daemon-message-pipeline.js +268 -0
- package/scripts/daemon-ops-commands.js +12 -10
- package/scripts/daemon-reactive-lifecycle.js +421 -0
- package/scripts/daemon-session-store.js +24 -24
- package/scripts/daemon-task-scheduler.js +90 -112
- package/scripts/daemon-warm-pool.js +162 -0
- package/scripts/daemon-worktrees.js +129 -0
- package/scripts/daemon.js +31 -3
- package/scripts/daemon.yaml +89 -1
- package/scripts/verify-reactive-claude-md.js +101 -0
package/package.json
CHANGED
|
@@ -20,11 +20,13 @@ function createBridgeStarter(deps) {
|
|
|
20
20
|
getSession,
|
|
21
21
|
restoreSessionFromReply,
|
|
22
22
|
handleCommand,
|
|
23
|
+
pipeline, // message pipeline for per-chatId serial execution
|
|
23
24
|
pendingActivations, // optional — used to show smart activation hint
|
|
24
|
-
activeProcesses,
|
|
25
|
-
messageQueue,
|
|
25
|
+
activeProcesses: _activeProcesses, // legacy — now handled by pipeline
|
|
26
|
+
messageQueue: _messageQueue, // legacy — now handled by pipeline
|
|
26
27
|
sendRemoteDispatch, // optional — send packet to remote peer via relay chat
|
|
27
28
|
handleRemoteDispatchMessage, // optional — intercept relay chat messages
|
|
29
|
+
getOrCreateWorktree, // optional — isolated worktree per actor
|
|
28
30
|
} = deps;
|
|
29
31
|
|
|
30
32
|
async function sendAclReply(bot, chatId, text) {
|
|
@@ -278,19 +280,26 @@ function createBridgeStarter(deps) {
|
|
|
278
280
|
const virtualChatId = `_agent_${member.key}`;
|
|
279
281
|
const parentCwd = member.cwd || boundProj.cwd;
|
|
280
282
|
const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
|
|
281
|
-
const memberCwd =
|
|
283
|
+
const memberCwd = typeof getOrCreateWorktree === 'function'
|
|
284
|
+
? getOrCreateWorktree(resolvedParentCwd, member.key)
|
|
285
|
+
: _getMemberCwd(resolvedParentCwd, member.key);
|
|
282
286
|
if (!memberCwd) {
|
|
283
287
|
log('ERROR', `Team [${member.key}] cannot start: directory unavailable`);
|
|
284
288
|
bot.sendMessage(realChatId, `❌ ${member.icon || '🤖'} ${member.name} 启动失败:工作目录创建失败`).catch(() => {});
|
|
285
289
|
return;
|
|
286
290
|
}
|
|
287
291
|
log('INFO', `Team [${member.key}] using cwd: ${memberCwd}`);
|
|
292
|
+
// Spawn cwd MUST be the actual work directory (worktree/member dir) so that:
|
|
293
|
+
// 1. Claude CLI operates in the correct directory (git, file edits)
|
|
294
|
+
// 2. /undo, /redo, /reset target the right repo
|
|
295
|
+
// Session visibility on desktop is handled by findSessionFile scanning all project dirs,
|
|
296
|
+
// and by session naming (auto-name with agent label prefix).
|
|
288
297
|
const teamCfg = {
|
|
289
298
|
...cfg,
|
|
290
299
|
projects: {
|
|
291
300
|
...(cfg.projects || {}),
|
|
292
301
|
[member.key]: {
|
|
293
|
-
cwd: memberCwd,
|
|
302
|
+
cwd: memberCwd, // actual work directory
|
|
294
303
|
name: member.name,
|
|
295
304
|
icon: member.icon || '🤖',
|
|
296
305
|
color: member.color || 'blue',
|
|
@@ -299,7 +308,7 @@ function createBridgeStarter(deps) {
|
|
|
299
308
|
},
|
|
300
309
|
};
|
|
301
310
|
const proxyBot = _createTeamProxyBot(bot, realChatId);
|
|
302
|
-
|
|
311
|
+
pipeline.processMessage(virtualChatId, text, { bot: proxyBot, config: teamCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly })
|
|
303
312
|
.catch(e => log('ERROR', `Team [${member.key}] error: ${e.message}`));
|
|
304
313
|
}
|
|
305
314
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -359,7 +368,7 @@ function createBridgeStarter(deps) {
|
|
|
359
368
|
bypassAcl: !allowedIds.includes(chatId) && !!isBindCmd,
|
|
360
369
|
});
|
|
361
370
|
if (acl.blocked) continue;
|
|
362
|
-
|
|
371
|
+
pipeline.processMessage(chatId, cb.data, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
|
|
363
372
|
log('ERROR', `Telegram callback handler error: ${e.message}`);
|
|
364
373
|
});
|
|
365
374
|
}
|
|
@@ -434,7 +443,7 @@ function createBridgeStarter(deps) {
|
|
|
434
443
|
continue;
|
|
435
444
|
}
|
|
436
445
|
}
|
|
437
|
-
|
|
446
|
+
pipeline.processMessage(chatId, prompt, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
|
|
438
447
|
log('ERROR', `Telegram file handler error: ${e.message}`);
|
|
439
448
|
});
|
|
440
449
|
} catch (err) {
|
|
@@ -515,16 +524,9 @@ function createBridgeStarter(deps) {
|
|
|
515
524
|
const vid = `_agent_${_targetKey}`;
|
|
516
525
|
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
517
526
|
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
messageQueue.delete(vid);
|
|
522
|
-
}
|
|
523
|
-
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
524
|
-
if (vproc && vproc.child) {
|
|
525
|
-
vproc.aborted = true;
|
|
526
|
-
const sig = vproc.killSignal || 'SIGTERM';
|
|
527
|
-
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
527
|
+
pipeline.clearQueue(vid);
|
|
528
|
+
const stopped = pipeline.interruptActive(vid);
|
|
529
|
+
if (stopped) {
|
|
528
530
|
await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
|
|
529
531
|
} else {
|
|
530
532
|
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
@@ -577,7 +579,7 @@ function createBridgeStarter(deps) {
|
|
|
577
579
|
continue;
|
|
578
580
|
}
|
|
579
581
|
try {
|
|
580
|
-
await
|
|
582
|
+
await pipeline.processMessage(chatId, rest, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
581
583
|
} catch (e) {
|
|
582
584
|
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
583
585
|
bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
@@ -597,7 +599,7 @@ function createBridgeStarter(deps) {
|
|
|
597
599
|
}
|
|
598
600
|
|
|
599
601
|
// Default: route to main project
|
|
600
|
-
|
|
602
|
+
pipeline.processMessage(chatId, text, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
|
|
601
603
|
log('ERROR', `Telegram handler error: ${e.message}`);
|
|
602
604
|
});
|
|
603
605
|
}
|
|
@@ -727,7 +729,7 @@ function createBridgeStarter(deps) {
|
|
|
727
729
|
return;
|
|
728
730
|
}
|
|
729
731
|
}
|
|
730
|
-
await
|
|
732
|
+
await pipeline.processMessage(chatId, prompt, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
731
733
|
} catch (err) {
|
|
732
734
|
log('ERROR', `Feishu file download failed: ${err.message}`);
|
|
733
735
|
await bot.sendMessage(chatId, `❌ Download failed: ${err.message}`);
|
|
@@ -816,17 +818,9 @@ function createBridgeStarter(deps) {
|
|
|
816
818
|
const vid = `_agent_${_targetKey}`;
|
|
817
819
|
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
818
820
|
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
823
|
-
messageQueue.delete(vid);
|
|
824
|
-
}
|
|
825
|
-
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
826
|
-
if (vproc && vproc.child) {
|
|
827
|
-
vproc.aborted = true;
|
|
828
|
-
const sig = vproc.killSignal || 'SIGTERM';
|
|
829
|
-
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
821
|
+
pipeline.clearQueue(vid);
|
|
822
|
+
const stopped = pipeline.interruptActive(vid);
|
|
823
|
+
if (stopped) {
|
|
830
824
|
await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
|
|
831
825
|
} else {
|
|
832
826
|
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
@@ -902,7 +896,7 @@ function createBridgeStarter(deps) {
|
|
|
902
896
|
return;
|
|
903
897
|
}
|
|
904
898
|
try {
|
|
905
|
-
await
|
|
899
|
+
await pipeline.processMessage(chatId, rest, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
906
900
|
} catch (e) {
|
|
907
901
|
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
908
902
|
bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
@@ -922,7 +916,12 @@ function createBridgeStarter(deps) {
|
|
|
922
916
|
|
|
923
917
|
}
|
|
924
918
|
|
|
925
|
-
|
|
919
|
+
try {
|
|
920
|
+
await pipeline.processMessage(chatId, text, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
921
|
+
} catch (e) {
|
|
922
|
+
log('ERROR', `Feishu handleCommand failed for ${chatId}: ${e.message}`);
|
|
923
|
+
bot.sendMessage(chatId, `❌ 命令执行失败: ${e.message}`).catch(() => {});
|
|
924
|
+
}
|
|
926
925
|
}
|
|
927
926
|
}, { log: (lvl, msg) => log(lvl, msg) });
|
|
928
927
|
|
|
@@ -1071,16 +1070,9 @@ function createBridgeStarter(deps) {
|
|
|
1071
1070
|
const vid = `_agent_${_targetKey}`;
|
|
1072
1071
|
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
1073
1072
|
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
messageQueue.delete(vid);
|
|
1078
|
-
}
|
|
1079
|
-
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
1080
|
-
if (vproc && vproc.child) {
|
|
1081
|
-
vproc.aborted = true;
|
|
1082
|
-
const sig = vproc.killSignal || 'SIGTERM';
|
|
1083
|
-
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
1073
|
+
pipeline.clearQueue(vid);
|
|
1074
|
+
const stopped = pipeline.interruptActive(vid);
|
|
1075
|
+
if (stopped) {
|
|
1084
1076
|
await bot.sendMessage(chatId, `Stopping ${label}...`);
|
|
1085
1077
|
} else {
|
|
1086
1078
|
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
@@ -1132,7 +1124,7 @@ function createBridgeStarter(deps) {
|
|
|
1132
1124
|
}
|
|
1133
1125
|
}
|
|
1134
1126
|
|
|
1135
|
-
|
|
1127
|
+
pipeline.processMessage(chatId, commandText, { bot, config: liveCfg, executeTaskByName, senderId: sender, readOnly: false })
|
|
1136
1128
|
.catch(e => log('ERROR', `[IMESSAGE] handleCommand error: ${e.message}`));
|
|
1137
1129
|
}
|
|
1138
1130
|
} catch (e) {
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function createCheckpointUtils(deps) {
|
|
4
|
-
const { execSync, execFile, path, log } = deps;
|
|
4
|
+
const { execSync: _execSync, execFile, path, log } = deps;
|
|
5
5
|
const { promisify } = require('util');
|
|
6
6
|
const execFileAsync = execFile ? promisify(execFile) : null;
|
|
7
7
|
|
|
8
8
|
const CHECKPOINT_PREFIX = '[metame-checkpoint]';
|
|
9
9
|
const CHECKPOINT_REF_PREFIX = 'refs/metame/checkpoints/';
|
|
10
|
+
|
|
11
|
+
// Build the ref path for a checkpoint.
|
|
12
|
+
// When agentKey is provided: refs/metame/checkpoints/<agentKey>/<ts>
|
|
13
|
+
// Otherwise: refs/metame/checkpoints/<ts> (backward compat)
|
|
14
|
+
function _checkpointRef(ts, agentKey) {
|
|
15
|
+
if (agentKey && String(agentKey).trim()) {
|
|
16
|
+
const safe = String(agentKey).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 60);
|
|
17
|
+
return `${CHECKPOINT_REF_PREFIX}${safe}/${ts}`;
|
|
18
|
+
}
|
|
19
|
+
return `${CHECKPOINT_REF_PREFIX}${ts}`;
|
|
20
|
+
}
|
|
10
21
|
const MAX_CHECKPOINTS = 20;
|
|
11
22
|
|
|
12
23
|
function cpExtractTimestamp(message) {
|
|
@@ -50,41 +61,41 @@ function createCheckpointUtils(deps) {
|
|
|
50
61
|
return { ts, safeLabel, msg: `${CHECKPOINT_PREFIX}${safeLabel} (${ts})` };
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
// Build a checkpoint commit stored under refs/metame/checkpoints/{ts} — never pushed by git push.
|
|
64
|
+
// Build a checkpoint commit stored under refs/metame/checkpoints/{agentKey}/{ts} — never pushed by git push.
|
|
54
65
|
// Returns the commit SHA, or null if nothing changed.
|
|
55
|
-
function gitCheckpoint(cwd, label) {
|
|
66
|
+
function gitCheckpoint(cwd, label, agentKey) {
|
|
67
|
+
const { execFileSync } = require('child_process');
|
|
56
68
|
try {
|
|
57
|
-
|
|
69
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'ignore', ...WIN_HIDE });
|
|
58
70
|
|
|
59
71
|
// Snapshot current index so we can restore it after staging
|
|
60
|
-
const originalTree =
|
|
72
|
+
const originalTree = execFileSync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim();
|
|
61
73
|
|
|
62
74
|
// Stage everything to get a full snapshot tree
|
|
63
|
-
|
|
64
|
-
const cpTree =
|
|
75
|
+
execFileSync('git', ['add', '-A'], { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
|
|
76
|
+
const cpTree = execFileSync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim();
|
|
65
77
|
|
|
66
78
|
// Restore index immediately — leave the user's staged state intact
|
|
67
|
-
|
|
79
|
+
execFileSync('git', ['read-tree', originalTree], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
|
|
68
80
|
|
|
69
81
|
// Compare against HEAD tree — skip if nothing changed
|
|
70
82
|
let headTree = '';
|
|
71
|
-
try { headTree =
|
|
83
|
+
try { headTree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim(); } catch { /* no commits yet */ }
|
|
72
84
|
if (cpTree === headTree) return null;
|
|
73
85
|
|
|
74
86
|
const { ts, safeLabel, msg } = buildCheckpointMsg(label);
|
|
75
87
|
|
|
76
|
-
// Build parent
|
|
77
|
-
let
|
|
78
|
-
try {
|
|
88
|
+
// Build parent args (-p HEAD, or empty for initial commit)
|
|
89
|
+
let parentArgs = [];
|
|
90
|
+
try { parentArgs = ['-p', execFileSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim()]; } catch { /* no HEAD */ }
|
|
79
91
|
|
|
80
92
|
// Create an orphaned commit object — NOT on any branch
|
|
81
|
-
const cpSha =
|
|
82
|
-
`git commit-tree ${cpTree} ${parentFlag} -m "${msg}"`,
|
|
93
|
+
const cpSha = execFileSync('git', ['commit-tree', cpTree, ...parentArgs, '-m', msg],
|
|
83
94
|
{ cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE }
|
|
84
|
-
).trim();
|
|
95
|
+
).toString().trim();
|
|
85
96
|
|
|
86
97
|
// Point a local-only ref at it — git push never transfers refs/metame/*
|
|
87
|
-
|
|
98
|
+
execFileSync('git', ['update-ref', _checkpointRef(ts, agentKey), cpSha], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
|
|
88
99
|
|
|
89
100
|
log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
|
|
90
101
|
return cpSha;
|
|
@@ -94,8 +105,8 @@ function createCheckpointUtils(deps) {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
// Async version: same logic but non-blocking.
|
|
97
|
-
async function gitCheckpointAsync(cwd, label) {
|
|
98
|
-
if (!execFileAsync) return gitCheckpoint(cwd, label);
|
|
108
|
+
async function gitCheckpointAsync(cwd, label, agentKey) {
|
|
109
|
+
if (!execFileAsync) return gitCheckpoint(cwd, label, agentKey);
|
|
99
110
|
try {
|
|
100
111
|
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 3000, ...WIN_HIDE });
|
|
101
112
|
|
|
@@ -121,7 +132,7 @@ function createCheckpointUtils(deps) {
|
|
|
121
132
|
const { stdout: cpShaOut } = await execFileAsync('git', ['commit-tree', cpTree, ...parentArgs, '-m', msg], { cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE });
|
|
122
133
|
const cpSha = cpShaOut.trim();
|
|
123
134
|
|
|
124
|
-
await execFileAsync('git', ['update-ref',
|
|
135
|
+
await execFileAsync('git', ['update-ref', _checkpointRef(ts, agentKey), cpSha], { cwd, timeout: 3000, ...WIN_HIDE });
|
|
125
136
|
|
|
126
137
|
log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
|
|
127
138
|
return cpSha;
|
|
@@ -133,11 +144,13 @@ function createCheckpointUtils(deps) {
|
|
|
133
144
|
// List checkpoints, newest first. Returns [{hash, message, ref, parentHash}].
|
|
134
145
|
// Uses %(parent) in for-each-ref format — no extra subprocess per checkpoint.
|
|
135
146
|
function listCheckpoints(cwd, limit = 20) {
|
|
147
|
+
const { execFileSync } = require('child_process');
|
|
136
148
|
try {
|
|
137
|
-
const raw =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
const raw = execFileSync('git', [
|
|
150
|
+
'for-each-ref', '--sort=-committerdate',
|
|
151
|
+
`--format=%(objectname)|%(refname)|%(parent)|%(contents:subject)`,
|
|
152
|
+
`--count=${limit}`, CHECKPOINT_REF_PREFIX,
|
|
153
|
+
], { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }).toString().trim();
|
|
141
154
|
if (!raw) return [];
|
|
142
155
|
return raw.split('\n').filter(Boolean).map(line => {
|
|
143
156
|
const [hash, ref, parent, ...rest] = line.split('|');
|
|
@@ -148,12 +161,13 @@ function createCheckpointUtils(deps) {
|
|
|
148
161
|
|
|
149
162
|
// Delete checkpoints beyond MAX_CHECKPOINTS (oldest first).
|
|
150
163
|
function cleanupCheckpoints(cwd) {
|
|
164
|
+
const { execFileSync } = require('child_process');
|
|
151
165
|
try {
|
|
152
166
|
const all = listCheckpoints(cwd, 100);
|
|
153
167
|
if (all.length <= MAX_CHECKPOINTS) return;
|
|
154
168
|
const toDelete = all.slice(MAX_CHECKPOINTS); // oldest (for-each-ref sorted newest-first)
|
|
155
169
|
for (const cp of toDelete) {
|
|
156
|
-
try {
|
|
170
|
+
try { execFileSync('git', ['update-ref', '-d', cp.ref], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE }); } catch { /* ignore */ }
|
|
157
171
|
}
|
|
158
172
|
log('INFO', `Cleaned up ${toDelete.length} old checkpoints in ${path.basename(cwd)}`);
|
|
159
173
|
} catch { /* ignore */ }
|