metame-cli 1.5.19 → 1.5.20
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 +79 -35
- 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
|
@@ -4,9 +4,12 @@ let userAcl = null;
|
|
|
4
4
|
try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
|
|
5
5
|
const { findTeamMember: _findTeamMember } = require('./daemon-team-dispatch');
|
|
6
6
|
const { isRemoteMember } = require('./daemon-remote-dispatch');
|
|
7
|
+
const { buildThreadChatId, rawChatId: _threadRawChatId } = require('./core/thread-chat-id');
|
|
7
8
|
const imessageIO = (() => { try { return require('./daemon-siri-imessage'); } catch { return null; } })();
|
|
8
9
|
const siriBridgeMod = (() => { try { return require('./daemon-siri-bridge'); } catch { return null; } })();
|
|
9
10
|
const weixinBridgeMod = (() => { try { return require('./daemon-weixin-bridge'); } catch { return null; } })();
|
|
11
|
+
const MSG_SESSION_MAX_ENTRIES = 5000;
|
|
12
|
+
const MSG_SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
10
13
|
|
|
11
14
|
function createBridgeStarter(deps) {
|
|
12
15
|
const {
|
|
@@ -114,6 +117,20 @@ function createBridgeStarter(deps) {
|
|
|
114
117
|
return null;
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Extract the topic root message ID (root_id) from a Feishu event.
|
|
122
|
+
* Returns non-null ONLY for messages inside a Feishu "话题" thread,
|
|
123
|
+
* NOT for plain quoted replies in conversation mode.
|
|
124
|
+
*/
|
|
125
|
+
function extractFeishuThreadRootId(event) {
|
|
126
|
+
const msg = (event && event.message) || (event && event.event && event.event.message);
|
|
127
|
+
if (!msg) return null;
|
|
128
|
+
// root_id is set when the message belongs to a topic thread.
|
|
129
|
+
// In conversation mode, a simple "指定回复" sets parent_id but NOT root_id.
|
|
130
|
+
const rootId = String(msg.root_id || '').trim();
|
|
131
|
+
return rootId || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
function trackBridgeReplyMapping(messageId, payload = {}) {
|
|
118
135
|
const safeMessageId = String(messageId || '').trim();
|
|
119
136
|
if (!safeMessageId) return;
|
|
@@ -122,7 +139,20 @@ function createBridgeStarter(deps) {
|
|
|
122
139
|
state.msg_sessions[safeMessageId] = {
|
|
123
140
|
...(state.msg_sessions[safeMessageId] || {}),
|
|
124
141
|
...payload,
|
|
142
|
+
touchedAt: Date.now(),
|
|
125
143
|
};
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const entries = Object.entries(state.msg_sessions).filter(([, value]) => {
|
|
146
|
+
const touchedAt = Number(value && value.touchedAt || 0);
|
|
147
|
+
return !touchedAt || (now - touchedAt) <= MSG_SESSION_MAX_AGE_MS;
|
|
148
|
+
});
|
|
149
|
+
state.msg_sessions = Object.fromEntries(
|
|
150
|
+
(entries.length > MSG_SESSION_MAX_ENTRIES
|
|
151
|
+
? entries
|
|
152
|
+
.sort((a, b) => Number((a[1] && a[1].touchedAt) || 0) - Number((b[1] && b[1].touchedAt) || 0))
|
|
153
|
+
.slice(entries.length - MSG_SESSION_MAX_ENTRIES)
|
|
154
|
+
: entries)
|
|
155
|
+
);
|
|
126
156
|
saveState(state);
|
|
127
157
|
}
|
|
128
158
|
|
|
@@ -160,10 +190,9 @@ function createBridgeStarter(deps) {
|
|
|
160
190
|
const map = {
|
|
161
191
|
...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
|
|
162
192
|
...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
|
|
163
|
-
...(cfg.weixin ? cfg.weixin.chat_agent_map || {} : {}),
|
|
164
193
|
...(cfg.imessage ? cfg.imessage.chat_agent_map || {} : {}),
|
|
165
194
|
};
|
|
166
|
-
const key = map[String(chatId)];
|
|
195
|
+
const key = map[String(chatId)] || map[_threadRawChatId(chatId)];
|
|
167
196
|
const proj = key && cfg.projects ? cfg.projects[key] : null;
|
|
168
197
|
return { key: key || null, project: proj || null };
|
|
169
198
|
}
|
|
@@ -181,9 +210,9 @@ function createBridgeStarter(deps) {
|
|
|
181
210
|
},
|
|
182
211
|
});
|
|
183
212
|
}
|
|
184
|
-
// Get team member's working directory
|
|
185
|
-
// Creates agents/<key>/ directory
|
|
186
|
-
function _getMemberCwd(parentCwd, key) {
|
|
213
|
+
// Get team member's working directory inside the source tree, never under ~/.metame.
|
|
214
|
+
// Creates agents/<key>/ directory by default, or ensures an explicit member.cwd exists.
|
|
215
|
+
function _getMemberCwd(parentCwd, key, explicitCwd = null) {
|
|
187
216
|
const { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync } = require('fs');
|
|
188
217
|
const { execFileSync } = require('child_process');
|
|
189
218
|
const WIN_HIDE = process.platform === 'win32' ? { windowsHide: true } : {};
|
|
@@ -194,12 +223,14 @@ function createBridgeStarter(deps) {
|
|
|
194
223
|
log('WARN', `Sanitized team member key: ${key} -> ${safeKey}`);
|
|
195
224
|
}
|
|
196
225
|
|
|
197
|
-
// Use
|
|
226
|
+
// Use explicit member cwd when provided, otherwise default to agents/<key>/.
|
|
198
227
|
const agentsDir = path.join(parentCwd, 'agents');
|
|
199
|
-
const memberDir =
|
|
228
|
+
const memberDir = explicitCwd
|
|
229
|
+
? path.resolve(String(explicitCwd).replace(/^~/, require('os').homedir()))
|
|
230
|
+
: path.join(agentsDir, safeKey);
|
|
200
231
|
|
|
201
|
-
// Create agents directory if
|
|
202
|
-
if (!existsSync(agentsDir)) {
|
|
232
|
+
// Create agents directory if using the default layout.
|
|
233
|
+
if (!explicitCwd && !existsSync(agentsDir)) {
|
|
203
234
|
mkdirSync(agentsDir, { recursive: true });
|
|
204
235
|
}
|
|
205
236
|
|
|
@@ -282,9 +313,11 @@ function createBridgeStarter(deps) {
|
|
|
282
313
|
const virtualChatId = `_agent_${member.key}`;
|
|
283
314
|
const parentCwd = member.cwd || boundProj.cwd;
|
|
284
315
|
const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
|
|
285
|
-
const memberCwd =
|
|
286
|
-
|
|
287
|
-
|
|
316
|
+
const memberCwd = _getMemberCwd(
|
|
317
|
+
resolvedParentCwd,
|
|
318
|
+
member.key,
|
|
319
|
+
member.cwd || null,
|
|
320
|
+
);
|
|
288
321
|
if (!memberCwd) {
|
|
289
322
|
log('ERROR', `Team [${member.key}] cannot start: directory unavailable`);
|
|
290
323
|
bot.sendMessage(realChatId, `❌ ${member.icon || '🤖'} ${member.name} 启动失败:工作目录创建失败`).catch(() => {});
|
|
@@ -710,6 +743,13 @@ function createBridgeStarter(deps) {
|
|
|
710
743
|
return;
|
|
711
744
|
}
|
|
712
745
|
|
|
746
|
+
// ── Topic mode detection (before file/text split) ──
|
|
747
|
+
const threadRootId = extractFeishuThreadRootId(event);
|
|
748
|
+
const pipelineChatId = threadRootId ? buildThreadChatId(chatId, threadRootId) : chatId;
|
|
749
|
+
if (threadRootId) {
|
|
750
|
+
log('INFO', `Feishu topic detected: root=${threadRootId} → pipelineChatId=${pipelineChatId}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
713
753
|
if (fileInfo && fileInfo.fileKey) {
|
|
714
754
|
const acl = await applyUserAcl({
|
|
715
755
|
bot,
|
|
@@ -721,7 +761,7 @@ function createBridgeStarter(deps) {
|
|
|
721
761
|
});
|
|
722
762
|
if (acl.blocked) return;
|
|
723
763
|
log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
|
|
724
|
-
const session = getSession(chatId);
|
|
764
|
+
const session = getSession(pipelineChatId) || getSession(chatId);
|
|
725
765
|
const cwd = session?.cwd || HOME;
|
|
726
766
|
const uploadDir = path.join(cwd, 'upload');
|
|
727
767
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
@@ -729,7 +769,7 @@ function createBridgeStarter(deps) {
|
|
|
729
769
|
|
|
730
770
|
try {
|
|
731
771
|
await bot.downloadFile(fileInfo.messageId, fileInfo.fileKey, destPath, fileInfo.msgType);
|
|
732
|
-
await bot.sendMessage(
|
|
772
|
+
await bot.sendMessage(pipelineChatId, `📥 Saved: ${fileInfo.fileName}`);
|
|
733
773
|
|
|
734
774
|
const prompt = text
|
|
735
775
|
? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
|
|
@@ -737,21 +777,21 @@ function createBridgeStarter(deps) {
|
|
|
737
777
|
|
|
738
778
|
// Respect team_sticky: route to active agent same as text messages
|
|
739
779
|
const _stFile = loadState();
|
|
740
|
-
const _chatKeyFile = String(
|
|
780
|
+
const _chatKeyFile = String(pipelineChatId);
|
|
741
781
|
const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
|
|
742
782
|
const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
|
|
743
783
|
if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
|
|
744
784
|
const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
|
|
745
785
|
if (_stickyMember) {
|
|
746
786
|
log('INFO', `Feishu file → sticky route to ${_stickyKeyFile}`);
|
|
747
|
-
_dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot,
|
|
787
|
+
_dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot, pipelineChatId, executeTaskByName, acl);
|
|
748
788
|
return;
|
|
749
789
|
}
|
|
750
790
|
}
|
|
751
|
-
await pipeline.processMessage(
|
|
791
|
+
await pipeline.processMessage(pipelineChatId, prompt, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
752
792
|
} catch (err) {
|
|
753
793
|
log('ERROR', `Feishu file download failed: ${err.message}`);
|
|
754
|
-
await bot.sendMessage(
|
|
794
|
+
await bot.sendMessage(pipelineChatId, `❌ Download failed: ${err.message}`);
|
|
755
795
|
}
|
|
756
796
|
return;
|
|
757
797
|
}
|
|
@@ -772,10 +812,13 @@ function createBridgeStarter(deps) {
|
|
|
772
812
|
let _replyMappingFound = false; // true = mapping exists (agentKey may be null = main)
|
|
773
813
|
// Load state once for the entire routing block
|
|
774
814
|
const _st = loadState();
|
|
775
|
-
|
|
815
|
+
// Quoted reply = explicit parentId but NOT a topic thread (topics always carry parentId=root_id)
|
|
816
|
+
const _isQuotedReply = !!(parentId && !threadRootId);
|
|
817
|
+
if (_isQuotedReply) {
|
|
776
818
|
log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}`);
|
|
777
819
|
}
|
|
778
|
-
|
|
820
|
+
// In topic mode, session continuity is handled by pipelineChatId — skip msg_sessions lookup
|
|
821
|
+
if (_isQuotedReply) {
|
|
779
822
|
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
780
823
|
if (mapped) {
|
|
781
824
|
_replyMappingFound = true;
|
|
@@ -798,7 +841,8 @@ function createBridgeStarter(deps) {
|
|
|
798
841
|
}
|
|
799
842
|
|
|
800
843
|
// Helper: set/clear sticky on shared state object and persist
|
|
801
|
-
|
|
844
|
+
// Use pipelineChatId so each topic gets independent sticky state
|
|
845
|
+
const _chatKey = String(pipelineChatId);
|
|
802
846
|
const _setSticky = (key) => {
|
|
803
847
|
if (!_st.team_sticky) _st.team_sticky = {};
|
|
804
848
|
_st.team_sticky[_chatKey] = key;
|
|
@@ -842,15 +886,15 @@ function createBridgeStarter(deps) {
|
|
|
842
886
|
pipeline.clearQueue(vid);
|
|
843
887
|
const stopped = pipeline.interruptActive(vid);
|
|
844
888
|
if (stopped) {
|
|
845
|
-
await bot.sendMessage(
|
|
889
|
+
await bot.sendMessage(pipelineChatId, `⏹ Stopping ${label}...`);
|
|
846
890
|
} else {
|
|
847
|
-
await bot.sendMessage(
|
|
891
|
+
await bot.sendMessage(pipelineChatId, `${label} 当前没有活跃任务`);
|
|
848
892
|
}
|
|
849
893
|
return;
|
|
850
894
|
}
|
|
851
895
|
// /stop <bad-nickname> → no match, report error instead of falling through
|
|
852
896
|
if (_stopArg) {
|
|
853
|
-
await bot.sendMessage(
|
|
897
|
+
await bot.sendMessage(pipelineChatId, `❌ 未找到团队成员: ${_stopArg}`);
|
|
854
898
|
return;
|
|
855
899
|
}
|
|
856
900
|
// Bare /stop, no sticky set → fall through to handleCommand
|
|
@@ -861,13 +905,13 @@ function createBridgeStarter(deps) {
|
|
|
861
905
|
// a) agentKey = known team member → route to that member (set sticky)
|
|
862
906
|
// b) agentKey = null, mapping found → user replied to main; clear sticky, route to main
|
|
863
907
|
// c) parentId present, no mapping → intent is explicit, avoid sticky; clear sticky, route to main
|
|
864
|
-
if (
|
|
908
|
+
if (_isQuotedReply) {
|
|
865
909
|
if (_replyAgentKey) {
|
|
866
910
|
const member = _boundProj.team.find(m => m.key === _replyAgentKey);
|
|
867
911
|
if (member) {
|
|
868
912
|
_setSticky(member.key);
|
|
869
913
|
log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
|
|
870
|
-
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot,
|
|
914
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, pipelineChatId, executeTaskByName, acl);
|
|
871
915
|
return;
|
|
872
916
|
}
|
|
873
917
|
// agentKey set but not a current team member → fall through to main
|
|
@@ -876,7 +920,7 @@ function createBridgeStarter(deps) {
|
|
|
876
920
|
// Cases b & c: no agentKey (main agent) or stale/unknown agentKey
|
|
877
921
|
_clearSticky();
|
|
878
922
|
log('INFO', `Quoted reply → route to main (agentKey=${_replyAgentKey} mappingFound=${_replyMappingFound})`);
|
|
879
|
-
await pipeline.processMessage(
|
|
923
|
+
await pipeline.processMessage(pipelineChatId, trimmedText, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
880
924
|
return;
|
|
881
925
|
}
|
|
882
926
|
// 1. Explicit nickname → route + set sticky
|
|
@@ -887,7 +931,7 @@ function createBridgeStarter(deps) {
|
|
|
887
931
|
if (!rest) {
|
|
888
932
|
// Pure nickname, no task — confirm member is online
|
|
889
933
|
log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
|
|
890
|
-
bot.sendMarkdown(
|
|
934
|
+
bot.sendMarkdown(pipelineChatId, `${member.icon || '🤖'} **${member.name}** 在线`)
|
|
891
935
|
.then((msg) => {
|
|
892
936
|
if (msg && msg.message_id) {
|
|
893
937
|
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(`_agent_${member.key}`, {
|
|
@@ -901,7 +945,7 @@ function createBridgeStarter(deps) {
|
|
|
901
945
|
return;
|
|
902
946
|
}
|
|
903
947
|
log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
|
|
904
|
-
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot,
|
|
948
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, pipelineChatId, executeTaskByName, acl);
|
|
905
949
|
return;
|
|
906
950
|
}
|
|
907
951
|
|
|
@@ -914,7 +958,7 @@ function createBridgeStarter(deps) {
|
|
|
914
958
|
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
915
959
|
log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
|
|
916
960
|
if (!rest) {
|
|
917
|
-
bot.sendMarkdown(
|
|
961
|
+
bot.sendMarkdown(pipelineChatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`)
|
|
918
962
|
.then((msg) => {
|
|
919
963
|
if (msg && msg.message_id) {
|
|
920
964
|
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(String(chatId), {
|
|
@@ -929,10 +973,10 @@ function createBridgeStarter(deps) {
|
|
|
929
973
|
return;
|
|
930
974
|
}
|
|
931
975
|
try {
|
|
932
|
-
await pipeline.processMessage(
|
|
976
|
+
await pipeline.processMessage(pipelineChatId, rest, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
933
977
|
} catch (e) {
|
|
934
978
|
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
935
|
-
bot.sendMessage(
|
|
979
|
+
bot.sendMessage(pipelineChatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
936
980
|
}
|
|
937
981
|
return;
|
|
938
982
|
}
|
|
@@ -942,7 +986,7 @@ function createBridgeStarter(deps) {
|
|
|
942
986
|
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
943
987
|
if (member) {
|
|
944
988
|
log('INFO', `Sticky route: → ${_stickyKey}`);
|
|
945
|
-
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot,
|
|
989
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, pipelineChatId, executeTaskByName, acl);
|
|
946
990
|
return;
|
|
947
991
|
}
|
|
948
992
|
}
|
|
@@ -950,10 +994,10 @@ function createBridgeStarter(deps) {
|
|
|
950
994
|
}
|
|
951
995
|
|
|
952
996
|
try {
|
|
953
|
-
await pipeline.processMessage(
|
|
997
|
+
await pipeline.processMessage(pipelineChatId, text, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
|
|
954
998
|
} catch (e) {
|
|
955
999
|
log('ERROR', `Feishu handleCommand failed for ${chatId}: ${e.message}`);
|
|
956
|
-
bot.sendMessage(
|
|
1000
|
+
bot.sendMessage(pipelineChatId, `❌ 命令执行失败: ${e.message}`).catch(() => {});
|
|
957
1001
|
}
|
|
958
1002
|
}
|
|
959
1003
|
}, { log: (lvl, msg) => log(lvl, msg) });
|