metame-cli 1.5.23 → 1.5.24
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 +3 -2
- package/scripts/core/team-session-route.js +164 -0
- package/scripts/daemon-agent-commands.js +31 -42
- package/scripts/daemon-bridges.js +50 -9
- package/scripts/daemon-claude-engine.js +13 -1
- package/scripts/daemon-command-router.js +24 -6
- package/scripts/daemon-command-session-route.js +13 -38
- package/scripts/daemon-reactive-lifecycle.js +6 -6
- package/scripts/daemon-session-commands.js +33 -43
- package/scripts/daemon-session-store.js +5 -2
- package/scripts/daemon-warm-pool.js +65 -14
- package/scripts/ops-mission-queue.js +24 -1
- package/scripts/ops-reactive-bootstrap.js +46 -2
- package/scripts/core/handoff.test.js +0 -1074
- package/scripts/core/memory-model.test.js +0 -486
- package/scripts/core/reactive-paths.test.js +0 -35
- package/scripts/core/reactive-prompt.test.js +0 -88
- package/scripts/core/reactive-signal.test.js +0 -88
- package/scripts/core/thread-chat-id.test.js +0 -113
- package/scripts/sync-readme.js +0 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.24",
|
|
4
4
|
"description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
11
|
"scripts/",
|
|
12
|
-
"!scripts
|
|
12
|
+
"!scripts/**/*.test.js",
|
|
13
13
|
"!scripts/test_daemon.js",
|
|
14
|
+
"!scripts/sync-readme.js",
|
|
14
15
|
"!scripts/hooks/test-*.js",
|
|
15
16
|
"!scripts/daemon.yaml",
|
|
16
17
|
"!scripts/daemon.yaml.bak",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { rawChatId: _rawChatId } = require('./thread-chat-id');
|
|
4
|
+
|
|
5
|
+
function buildBoundSessionChatId(projectKey) {
|
|
6
|
+
const key = String(projectKey || '').trim();
|
|
7
|
+
return key ? `_bound_${key}` : '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isAgentLogicalRouteForMember(logicalChatId, memberKey) {
|
|
11
|
+
const route = String(logicalChatId || '');
|
|
12
|
+
const key = String(memberKey || '').trim();
|
|
13
|
+
if (!route || !key) return false;
|
|
14
|
+
const base = `_agent_${key}`;
|
|
15
|
+
return route === base || route.startsWith(`${base}::`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveBoundTeamContext({ chatId, cfg, state, normalizeCwd }) {
|
|
19
|
+
const chatKey = String(chatId || '');
|
|
20
|
+
const rawChatKey = _rawChatId(chatKey);
|
|
21
|
+
const agentMap = {
|
|
22
|
+
...(cfg && cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
23
|
+
...(cfg && cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
24
|
+
...(cfg && cfg.imessage ? cfg.imessage.chat_agent_map : {}),
|
|
25
|
+
...(cfg && cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
|
|
26
|
+
};
|
|
27
|
+
const boundKey = agentMap[chatKey] || agentMap[rawChatKey] || null;
|
|
28
|
+
const boundProj = boundKey && cfg && cfg.projects ? cfg.projects[boundKey] : null;
|
|
29
|
+
const stickyKey = state && state.team_sticky
|
|
30
|
+
? (state.team_sticky[chatKey] || state.team_sticky[rawChatKey] || null)
|
|
31
|
+
: null;
|
|
32
|
+
const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
|
|
33
|
+
? boundProj.team.find((member) => member && member.key === stickyKey) || null
|
|
34
|
+
: null;
|
|
35
|
+
const stickyLogicalChatId = state && state.team_session_route
|
|
36
|
+
? (state.team_session_route[chatKey] || state.team_session_route[rawChatKey] || null)
|
|
37
|
+
: null;
|
|
38
|
+
const normalizedStickyCwd = stickyMember && stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : null;
|
|
39
|
+
const normalizedBoundCwd = boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
chatKey,
|
|
43
|
+
rawChatKey,
|
|
44
|
+
boundKey,
|
|
45
|
+
boundProj,
|
|
46
|
+
stickyKey,
|
|
47
|
+
stickyMember,
|
|
48
|
+
stickyLogicalChatId,
|
|
49
|
+
normalizedStickyCwd,
|
|
50
|
+
normalizedBoundCwd,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveSessionRoute({
|
|
55
|
+
chatId,
|
|
56
|
+
cfg,
|
|
57
|
+
state,
|
|
58
|
+
getSession,
|
|
59
|
+
normalizeCwd,
|
|
60
|
+
normalizeEngineName,
|
|
61
|
+
inferStoredEngine,
|
|
62
|
+
}) {
|
|
63
|
+
const ctx = resolveBoundTeamContext({ chatId, cfg, state, normalizeCwd });
|
|
64
|
+
|
|
65
|
+
if (ctx.stickyMember) {
|
|
66
|
+
return {
|
|
67
|
+
sessionChatId: isAgentLogicalRouteForMember(ctx.stickyLogicalChatId, ctx.stickyMember.key)
|
|
68
|
+
? ctx.stickyLogicalChatId
|
|
69
|
+
: `_agent_${ctx.stickyMember.key}`,
|
|
70
|
+
cwd: ctx.normalizedStickyCwd || ctx.normalizedBoundCwd,
|
|
71
|
+
engine: normalizeEngineName(ctx.stickyMember.engine || (ctx.boundProj && ctx.boundProj.engine)),
|
|
72
|
+
context: ctx,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ctx.boundProj) {
|
|
77
|
+
return {
|
|
78
|
+
sessionChatId: buildBoundSessionChatId(ctx.boundKey),
|
|
79
|
+
cwd: ctx.normalizedBoundCwd,
|
|
80
|
+
engine: normalizeEngineName(ctx.boundProj.engine),
|
|
81
|
+
context: ctx,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rawSession = getSession(chatId);
|
|
86
|
+
return {
|
|
87
|
+
sessionChatId: String(chatId || ''),
|
|
88
|
+
cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
|
|
89
|
+
engine: inferStoredEngine(rawSession),
|
|
90
|
+
context: ctx,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveResumeRouteForTarget({
|
|
95
|
+
chatId,
|
|
96
|
+
targetCwd,
|
|
97
|
+
cfg,
|
|
98
|
+
state,
|
|
99
|
+
normalizeCwd,
|
|
100
|
+
fallbackSessionChatId,
|
|
101
|
+
}) {
|
|
102
|
+
const ctx = resolveBoundTeamContext({ chatId, cfg, state, normalizeCwd });
|
|
103
|
+
const normalizedTargetCwd = targetCwd ? normalizeCwd(targetCwd) : null;
|
|
104
|
+
if (!ctx.boundProj || !Array.isArray(ctx.boundProj.team) || !normalizedTargetCwd) {
|
|
105
|
+
return { sessionChatId: fallbackSessionChatId, stickyKey: null, clearSticky: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const matchedMember = ctx.boundProj.team.find((member) => {
|
|
109
|
+
if (!member || !member.cwd) return false;
|
|
110
|
+
return normalizeCwd(member.cwd) === normalizedTargetCwd;
|
|
111
|
+
});
|
|
112
|
+
if (matchedMember) {
|
|
113
|
+
return {
|
|
114
|
+
sessionChatId: `_agent_${matchedMember.key}`,
|
|
115
|
+
stickyKey: matchedMember.key,
|
|
116
|
+
clearSticky: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (ctx.normalizedBoundCwd && ctx.normalizedBoundCwd === normalizedTargetCwd) {
|
|
121
|
+
return {
|
|
122
|
+
sessionChatId: buildBoundSessionChatId(ctx.boundKey),
|
|
123
|
+
stickyKey: ctx.stickyKey || null,
|
|
124
|
+
clearSticky: !!ctx.stickyKey,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
sessionChatId: buildBoundSessionChatId(ctx.boundKey),
|
|
130
|
+
stickyKey: null,
|
|
131
|
+
clearSticky: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyResumeRouteState(state, chatId, resumeRoute) {
|
|
136
|
+
const chatKey = String(chatId || '');
|
|
137
|
+
const rawChatKey = _rawChatId(chatKey);
|
|
138
|
+
if (resumeRoute.clearSticky && state.team_sticky) {
|
|
139
|
+
delete state.team_sticky[chatKey];
|
|
140
|
+
delete state.team_sticky[rawChatKey];
|
|
141
|
+
}
|
|
142
|
+
if (resumeRoute.clearSticky && state.team_session_route) {
|
|
143
|
+
delete state.team_session_route[chatKey];
|
|
144
|
+
delete state.team_session_route[rawChatKey];
|
|
145
|
+
}
|
|
146
|
+
if (!resumeRoute.stickyKey) return;
|
|
147
|
+
if (!state.team_sticky) state.team_sticky = {};
|
|
148
|
+
state.team_sticky[chatKey] = resumeRoute.stickyKey;
|
|
149
|
+
state.team_sticky[rawChatKey] = resumeRoute.stickyKey;
|
|
150
|
+
if (resumeRoute.sessionChatId && resumeRoute.sessionChatId.startsWith('_agent_')) {
|
|
151
|
+
if (!state.team_session_route) state.team_session_route = {};
|
|
152
|
+
state.team_session_route[chatKey] = resumeRoute.sessionChatId;
|
|
153
|
+
state.team_session_route[rawChatKey] = resumeRoute.sessionChatId;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
buildBoundSessionChatId,
|
|
159
|
+
isAgentLogicalRouteForMember,
|
|
160
|
+
resolveBoundTeamContext,
|
|
161
|
+
resolveSessionRoute,
|
|
162
|
+
resolveResumeRouteForTarget,
|
|
163
|
+
applyResumeRouteState,
|
|
164
|
+
};
|
|
@@ -18,6 +18,11 @@ const {
|
|
|
18
18
|
handleSoulCommand,
|
|
19
19
|
} = require('./daemon-agent-lifecycle');
|
|
20
20
|
const { parseTeamMembers, createTeamWorkspace } = require('./daemon-team-workflow');
|
|
21
|
+
const {
|
|
22
|
+
resolveSessionRoute: _resolveSessionRoute,
|
|
23
|
+
resolveResumeRouteForTarget: _resolveResumeRouteForTarget,
|
|
24
|
+
applyResumeRouteState: _applyResumeRouteState,
|
|
25
|
+
} = require('./core/team-session-route');
|
|
21
26
|
|
|
22
27
|
function createAgentCommandHandler(deps) {
|
|
23
28
|
const {
|
|
@@ -72,45 +77,16 @@ function createAgentCommandHandler(deps) {
|
|
|
72
77
|
return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
function buildBoundSessionChatId(projectKey) {
|
|
76
|
-
const key = String(projectKey || '').trim();
|
|
77
|
-
return key ? `_bound_${key}` : '';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
80
|
function getSessionRoute(chatId) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: null;
|
|
91
|
-
|
|
92
|
-
if (stickyMember) {
|
|
93
|
-
return {
|
|
94
|
-
sessionChatId: `_agent_${stickyMember.key}`,
|
|
95
|
-
cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
|
|
96
|
-
engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (boundProj) {
|
|
101
|
-
return {
|
|
102
|
-
sessionChatId: buildBoundSessionChatId(boundKey),
|
|
103
|
-
cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
|
|
104
|
-
engine: normalizeEngineName(boundProj.engine),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const rawSession = getSession(chatId);
|
|
109
|
-
return {
|
|
110
|
-
sessionChatId: String(chatId),
|
|
111
|
-
cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
|
|
112
|
-
engine: inferStoredEngine(rawSession),
|
|
113
|
-
};
|
|
81
|
+
return _resolveSessionRoute({
|
|
82
|
+
chatId,
|
|
83
|
+
cfg: loadConfig(),
|
|
84
|
+
state: loadState(),
|
|
85
|
+
getSession,
|
|
86
|
+
normalizeCwd,
|
|
87
|
+
normalizeEngineName,
|
|
88
|
+
inferStoredEngine,
|
|
89
|
+
});
|
|
114
90
|
}
|
|
115
91
|
|
|
116
92
|
function getCurrentEngine(chatId) {
|
|
@@ -170,6 +146,17 @@ function createAgentCommandHandler(deps) {
|
|
|
170
146
|
return null;
|
|
171
147
|
}
|
|
172
148
|
|
|
149
|
+
function resolveResumeRouteForSelection(chatId, route, targetCwd, cfg, state) {
|
|
150
|
+
return _resolveResumeRouteForTarget({
|
|
151
|
+
chatId,
|
|
152
|
+
targetCwd,
|
|
153
|
+
cfg,
|
|
154
|
+
state,
|
|
155
|
+
normalizeCwd,
|
|
156
|
+
fallbackSessionChatId: route.sessionChatId,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
173
160
|
async function autoCreateSessionOnEmptyResume(bot, chatId, cwd, engine) {
|
|
174
161
|
const resolvedCwd = cwd ? normalizeCwd(cwd) : null;
|
|
175
162
|
if (!resolvedCwd || !fs.existsSync(resolvedCwd) || typeof attachOrCreateSession !== 'function') {
|
|
@@ -389,10 +376,13 @@ function createAgentCommandHandler(deps) {
|
|
|
389
376
|
}
|
|
390
377
|
const sessionId = fullMatch.sessionId;
|
|
391
378
|
const cwd = fullMatch.projectPath || (curSession && curSession.cwd) || HOME;
|
|
379
|
+
const targetSessionId = sessionId;
|
|
380
|
+
const targetCwd = cwd;
|
|
392
381
|
|
|
393
382
|
const state2 = loadState();
|
|
394
383
|
const cfgForEngine = loadConfig();
|
|
395
|
-
const
|
|
384
|
+
const resumeRoute = resolveResumeRouteForSelection(chatId, route, targetCwd, cfgForEngine, state2);
|
|
385
|
+
const sessionKey = resumeRoute.sessionChatId;
|
|
396
386
|
const existing = state2.sessions[sessionKey] || {};
|
|
397
387
|
const existingEngine = normalizeEngineName(
|
|
398
388
|
existing.engine
|
|
@@ -405,8 +395,6 @@ function createAgentCommandHandler(deps) {
|
|
|
405
395
|
&& currentLogical
|
|
406
396
|
&& currentLogical.id
|
|
407
397
|
&& sessionId === currentLogical.id;
|
|
408
|
-
const targetSessionId = sessionId;
|
|
409
|
-
const targetCwd = cwd;
|
|
410
398
|
const existingEngines = existing.engines || {};
|
|
411
399
|
state2.sessions[sessionKey] = {
|
|
412
400
|
...existing,
|
|
@@ -416,6 +404,7 @@ function createAgentCommandHandler(deps) {
|
|
|
416
404
|
engine: engineByTargetCwd,
|
|
417
405
|
engines: { ...existingEngines, [engineByTargetCwd]: { id: targetSessionId, started: true } },
|
|
418
406
|
};
|
|
407
|
+
_applyResumeRouteState(state2, chatId, resumeRoute);
|
|
419
408
|
saveState(state2);
|
|
420
409
|
const name = fullMatch.customTitle;
|
|
421
410
|
const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || targetSessionId.slice(0, 8);
|
|
@@ -423,7 +412,7 @@ function createAgentCommandHandler(deps) {
|
|
|
423
412
|
// 读取最近对话片段,帮助确认是否切换到正确的 session
|
|
424
413
|
const recentCtx = getSessionRecentContext ? getSessionRecentContext(targetSessionId) : null;
|
|
425
414
|
const recentDialogue = getSessionRecentDialogue ? getSessionRecentDialogue(targetSessionId, 4) : null;
|
|
426
|
-
let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}
|
|
415
|
+
let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}\n🆔 \`${targetSessionId}\``;
|
|
427
416
|
if (selectedLogicalCurrent) {
|
|
428
417
|
msg += '\n\n已恢复当前智能体会话。';
|
|
429
418
|
}
|
|
@@ -5,6 +5,7 @@ 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
7
|
const { buildThreadChatId, isThreadChatId, rawChatId: _threadRawChatId } = require('./core/thread-chat-id');
|
|
8
|
+
const { isAgentLogicalRouteForMember } = require('./core/team-session-route');
|
|
8
9
|
const imessageIO = (() => { try { return require('./daemon-siri-imessage'); } catch { return null; } })();
|
|
9
10
|
const siriBridgeMod = (() => { try { return require('./daemon-siri-bridge'); } catch { return null; } })();
|
|
10
11
|
const weixinBridgeMod = (() => { try { return require('./daemon-weixin-bridge'); } catch { return null; } })();
|
|
@@ -185,6 +186,19 @@ function createBridgeStarter(deps) {
|
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
function resolveReplyStopChatId(targetKey, fallbackChatId, replyMapping) {
|
|
190
|
+
const resolvedFallback = String(fallbackChatId || '').trim();
|
|
191
|
+
const mapping = replyMapping && typeof replyMapping === 'object' ? replyMapping : null;
|
|
192
|
+
const logicalChatId = String(mapping && mapping.logicalChatId || '').trim();
|
|
193
|
+
if (!logicalChatId) return resolvedFallback;
|
|
194
|
+
if (!targetKey) return logicalChatId;
|
|
195
|
+
const expectedPrefix = `_agent_${String(targetKey).trim()}`;
|
|
196
|
+
if (logicalChatId === expectedPrefix || logicalChatId.startsWith(`${expectedPrefix}::`)) {
|
|
197
|
+
return logicalChatId;
|
|
198
|
+
}
|
|
199
|
+
return resolvedFallback;
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
// ── Team group helpers ─────────────────────────────────────────────────
|
|
189
203
|
function _getBoundProject(chatId, cfg) {
|
|
190
204
|
const map = {
|
|
@@ -313,9 +327,16 @@ function createBridgeStarter(deps) {
|
|
|
313
327
|
// When dispatching from a topic thread, include the thread ID in the
|
|
314
328
|
// virtual session key so each topic gets its own independent session.
|
|
315
329
|
const realChatIdStr = String(realChatId || '');
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
const state = loadState() || {};
|
|
331
|
+
const routeMap = state.team_session_route || {};
|
|
332
|
+
const rawChatKey = _threadRawChatId(realChatIdStr);
|
|
333
|
+
const preferredLogicalChatId = routeMap[realChatIdStr] || routeMap[rawChatKey] || '';
|
|
334
|
+
const expectedBaseChatId = `_agent_${member.key}`;
|
|
335
|
+
const virtualChatId = isAgentLogicalRouteForMember(preferredLogicalChatId, member.key)
|
|
336
|
+
? preferredLogicalChatId
|
|
337
|
+
: (isThreadChatId(realChatIdStr)
|
|
338
|
+
? `${expectedBaseChatId}::${realChatIdStr}`
|
|
339
|
+
: expectedBaseChatId);
|
|
319
340
|
const parentCwd = member.cwd || boundProj.cwd;
|
|
320
341
|
const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
|
|
321
342
|
const memberCwd = _getMemberCwd(
|
|
@@ -559,9 +580,9 @@ function createBridgeStarter(deps) {
|
|
|
559
580
|
);
|
|
560
581
|
if (m) _targetKey = m.key;
|
|
561
582
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const vid = `_agent_${_targetKey}
|
|
583
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
584
|
+
if (_targetKey) {
|
|
585
|
+
const vid = resolveReplyStopChatId(_targetKey, `_agent_${_targetKey}`, parentId ? (_st.msg_sessions && _st.msg_sessions[parentId]) : null);
|
|
565
586
|
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
566
587
|
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
567
588
|
pipeline.clearQueue(vid);
|
|
@@ -783,8 +804,9 @@ function createBridgeStarter(deps) {
|
|
|
783
804
|
// Respect team_sticky: route to active agent same as text messages
|
|
784
805
|
const _stFile = loadState();
|
|
785
806
|
const _chatKeyFile = String(pipelineChatId);
|
|
807
|
+
const _rawChatKeyFile = _threadRawChatId(_chatKeyFile);
|
|
786
808
|
const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
|
|
787
|
-
const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
|
|
809
|
+
const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile] || (_stFile.team_sticky || {})[_rawChatKeyFile];
|
|
788
810
|
if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
|
|
789
811
|
const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
|
|
790
812
|
if (_stickyMember) {
|
|
@@ -814,6 +836,7 @@ function createBridgeStarter(deps) {
|
|
|
814
836
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
815
837
|
const parentId = extractFeishuReplyMessageId(event);
|
|
816
838
|
let _replyAgentKey = null;
|
|
839
|
+
let _replyMapping = null;
|
|
817
840
|
let _replyMappingFound = false; // true = mapping exists (agentKey may be null = main)
|
|
818
841
|
// Load state once for the entire routing block
|
|
819
842
|
const _st = loadState();
|
|
@@ -826,6 +849,7 @@ function createBridgeStarter(deps) {
|
|
|
826
849
|
if (_isQuotedReply) {
|
|
827
850
|
const mapped = _parentMapping;
|
|
828
851
|
if (mapped) {
|
|
852
|
+
_replyMapping = mapped;
|
|
829
853
|
_replyMappingFound = true;
|
|
830
854
|
if (typeof restoreSessionFromReply === 'function') {
|
|
831
855
|
restoreSessionFromReply(chatId, mapped);
|
|
@@ -852,16 +876,29 @@ function createBridgeStarter(deps) {
|
|
|
852
876
|
// Helper: set/clear sticky on shared state object and persist
|
|
853
877
|
// Use pipelineChatId so each topic gets independent sticky state
|
|
854
878
|
const _chatKey = String(pipelineChatId);
|
|
879
|
+
const _rawChatKey = _threadRawChatId(_chatKey);
|
|
855
880
|
const _setSticky = (key) => {
|
|
856
881
|
if (!_st.team_sticky) _st.team_sticky = {};
|
|
857
882
|
_st.team_sticky[_chatKey] = key;
|
|
883
|
+
if (_rawChatKey && _rawChatKey !== _chatKey) _st.team_sticky[_rawChatKey] = key;
|
|
884
|
+
if (_st.team_session_route) {
|
|
885
|
+
if (_st.team_session_route[_chatKey] && !isAgentLogicalRouteForMember(_st.team_session_route[_chatKey], key)) {
|
|
886
|
+
delete _st.team_session_route[_chatKey];
|
|
887
|
+
}
|
|
888
|
+
if (_rawChatKey && _rawChatKey !== _chatKey && _st.team_session_route[_rawChatKey] && !isAgentLogicalRouteForMember(_st.team_session_route[_rawChatKey], key)) {
|
|
889
|
+
delete _st.team_session_route[_rawChatKey];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
858
892
|
saveState(_st);
|
|
859
893
|
};
|
|
860
894
|
const _clearSticky = () => {
|
|
861
895
|
if (_st.team_sticky) delete _st.team_sticky[_chatKey];
|
|
896
|
+
if (_st.team_sticky && _rawChatKey && _rawChatKey !== _chatKey) delete _st.team_sticky[_rawChatKey];
|
|
897
|
+
if (_st.team_session_route) delete _st.team_session_route[_chatKey];
|
|
898
|
+
if (_st.team_session_route && _rawChatKey && _rawChatKey !== _chatKey) delete _st.team_session_route[_rawChatKey];
|
|
862
899
|
saveState(_st);
|
|
863
900
|
};
|
|
864
|
-
let _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
|
|
901
|
+
let _stickyKey = (_st.team_sticky || {})[_chatKey] || (_st.team_sticky || {})[_rawChatKey] || null;
|
|
865
902
|
|
|
866
903
|
// Team group routing: if bound project has a team array, check message for member nickname
|
|
867
904
|
// Non-/stop slash commands bypass team routing → handled by main project
|
|
@@ -898,9 +935,10 @@ function createBridgeStarter(deps) {
|
|
|
898
935
|
// Priority 3: bare /stop → sticky
|
|
899
936
|
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
900
937
|
if (_targetKey) {
|
|
901
|
-
const
|
|
938
|
+
const fallbackVid = isThreadChatId(String(pipelineChatId))
|
|
902
939
|
? `_agent_${_targetKey}::${pipelineChatId}`
|
|
903
940
|
: `_agent_${_targetKey}`;
|
|
941
|
+
const vid = resolveReplyStopChatId(_targetKey, fallbackVid, _isQuotedReply ? _replyMapping : null);
|
|
904
942
|
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
905
943
|
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
906
944
|
pipeline.clearQueue(vid);
|
|
@@ -1008,6 +1046,9 @@ function createBridgeStarter(deps) {
|
|
|
1008
1046
|
if (_stickyKey) {
|
|
1009
1047
|
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
1010
1048
|
if (member) {
|
|
1049
|
+
if ((_st.team_sticky || {})[_chatKey] !== _stickyKey) {
|
|
1050
|
+
_setSticky(_stickyKey);
|
|
1051
|
+
}
|
|
1011
1052
|
log('INFO', `Sticky route: → ${_stickyKey}`);
|
|
1012
1053
|
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, pipelineChatId, executeTaskByName, acl);
|
|
1013
1054
|
return;
|
|
@@ -1238,7 +1238,19 @@ function createClaudeEngine(deps) {
|
|
|
1238
1238
|
? () => bot.sendCard(chatId, { title: _ackCardHeader.title, body: '🤔', color: _ackCardHeader.color })
|
|
1239
1239
|
: () => (bot.sendMarkdown ? bot.sendMarkdown(chatId, '🤔') : bot.sendMessage(chatId, '🤔'));
|
|
1240
1240
|
_ackFn()
|
|
1241
|
-
.then(msg => {
|
|
1241
|
+
.then(msg => {
|
|
1242
|
+
if (!(msg && msg.message_id)) return;
|
|
1243
|
+
statusMsgId = msg.message_id;
|
|
1244
|
+
const trackedAgentKey = projectKeyFromVirtualChatId(chatId);
|
|
1245
|
+
const routeSession = {
|
|
1246
|
+
cwd: (_ackBoundProj && _ackBoundProj.cwd) || HOME,
|
|
1247
|
+
engine: (_ackBoundProj && _ackBoundProj.engine)
|
|
1248
|
+
? normalizeEngineName(_ackBoundProj.engine)
|
|
1249
|
+
: getDefaultEngine(),
|
|
1250
|
+
logicalChatId: chatId,
|
|
1251
|
+
};
|
|
1252
|
+
trackMsgSession(msg.message_id, routeSession, trackedAgentKey, { routeOnly: true });
|
|
1253
|
+
})
|
|
1242
1254
|
.catch(e => log('ERROR', `Failed to send ack to ${chatId}: ${e.message}`));
|
|
1243
1255
|
}
|
|
1244
1256
|
bot.sendTyping(chatId).catch(() => { });
|
|
@@ -163,6 +163,7 @@ function createCommandRouter(deps) {
|
|
|
163
163
|
|
|
164
164
|
function resolveCurrentSessionContext(chatId, config) {
|
|
165
165
|
const chatIdStr = String(chatId || '');
|
|
166
|
+
const threadScoped = isThreadChatId(chatIdStr);
|
|
166
167
|
const chatAgentMap = {
|
|
167
168
|
...(config && config.telegram ? config.telegram.chat_agent_map : {}),
|
|
168
169
|
...(config && config.feishu ? config.feishu.chat_agent_map : {}),
|
|
@@ -172,10 +173,19 @@ function createCommandRouter(deps) {
|
|
|
172
173
|
const _rawChatId = extractOriginalChatId(chatIdStr);
|
|
173
174
|
const mappedKey = chatAgentMap[chatIdStr] || chatAgentMap[_rawChatId] || projectKeyFromVirtualChatId(chatIdStr);
|
|
174
175
|
const mappedProject = mappedKey && config && config.projects ? config.projects[mappedKey] : null;
|
|
175
|
-
const preferredEngine = String((mappedProject && mappedProject.engine) || getDefaultEngine()).toLowerCase();
|
|
176
176
|
const state = loadState() || {};
|
|
177
177
|
const sessions = state.sessions || {};
|
|
178
|
+
const stickyKey = state.team_sticky ? (state.team_sticky[chatIdStr] || state.team_sticky[_rawChatId]) : null;
|
|
179
|
+
const stickyMember = threadScoped && stickyKey && mappedProject && Array.isArray(mappedProject.team)
|
|
180
|
+
? mappedProject.team.find((member) => member && member.key === stickyKey)
|
|
181
|
+
: null;
|
|
182
|
+
const preferredEngine = String(
|
|
183
|
+
(stickyMember && stickyMember.engine)
|
|
184
|
+
|| (mappedProject && mappedProject.engine)
|
|
185
|
+
|| getDefaultEngine()
|
|
186
|
+
).toLowerCase();
|
|
178
187
|
const candidateIds = [
|
|
188
|
+
stickyMember ? `_agent_${stickyMember.key}` : null,
|
|
179
189
|
mappedKey ? buildSessionChatId(chatIdStr, mappedKey) : null,
|
|
180
190
|
buildSessionChatId(chatIdStr),
|
|
181
191
|
chatIdStr,
|
|
@@ -212,7 +222,8 @@ function createCommandRouter(deps) {
|
|
|
212
222
|
...(cfg.imessage ? cfg.imessage.chat_agent_map : {}),
|
|
213
223
|
...(cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
|
|
214
224
|
};
|
|
215
|
-
const
|
|
225
|
+
const chatIdStr = String(chatId);
|
|
226
|
+
const key = map[chatIdStr] || map[extractOriginalChatId(chatIdStr)];
|
|
216
227
|
const proj = key && cfg.projects ? cfg.projects[key] : null;
|
|
217
228
|
return { key: key || null, project: proj || null };
|
|
218
229
|
}
|
|
@@ -387,17 +398,23 @@ function createCommandRouter(deps) {
|
|
|
387
398
|
};
|
|
388
399
|
const _chatIdStr = String(chatId);
|
|
389
400
|
const _rawChatId2 = extractOriginalChatId(_chatIdStr);
|
|
401
|
+
const _threadScoped = isThreadChatId(_chatIdStr);
|
|
390
402
|
const mappedKey = chatAgentMap[_chatIdStr] ||
|
|
391
403
|
chatAgentMap[_rawChatId2] ||
|
|
392
404
|
projectKeyFromVirtualChatId(_chatIdStr);
|
|
393
405
|
if (mappedKey && config.projects && config.projects[mappedKey]) {
|
|
394
406
|
const proj = config.projects[mappedKey];
|
|
395
|
-
const
|
|
396
|
-
const
|
|
407
|
+
const stickyKey = state && state.team_sticky ? (state.team_sticky[_chatIdStr] || state.team_sticky[_rawChatId2]) : null;
|
|
408
|
+
const stickyMember = _threadScoped && stickyKey && Array.isArray(proj.team)
|
|
409
|
+
? proj.team.find((member) => member && member.key === stickyKey)
|
|
410
|
+
: null;
|
|
411
|
+
const targetEngine = (stickyMember && stickyMember.engine) || proj.engine || getDefaultEngine();
|
|
412
|
+
const projCwd = normalizeCwd((stickyMember && stickyMember.cwd) || proj.cwd);
|
|
413
|
+
const sessionChatId = stickyMember ? `_agent_${stickyMember.key}` : buildSessionChatId(chatId, mappedKey);
|
|
397
414
|
const sessions = loadState().sessions || {};
|
|
398
415
|
const cur = sessions[sessionChatId];
|
|
399
416
|
const rawSession = sessions[String(chatId)];
|
|
400
|
-
const projEngine = String(
|
|
417
|
+
const projEngine = String(targetEngine).toLowerCase();
|
|
401
418
|
// Multi-engine format stores engines in cur.engines object; legacy format uses cur.engine string.
|
|
402
419
|
// Check whether the session already has a slot for the project's configured engine.
|
|
403
420
|
const curHasEngine = cur && (
|
|
@@ -408,6 +425,7 @@ function createCommandRouter(deps) {
|
|
|
408
425
|
);
|
|
409
426
|
const isVirtualSession = _chatIdStr.startsWith('_agent_') || _chatIdStr.startsWith('_scope_');
|
|
410
427
|
const shouldReattachForCwdChange =
|
|
428
|
+
!stickyMember &&
|
|
411
429
|
!isVirtualSession &&
|
|
412
430
|
!!cur &&
|
|
413
431
|
!!curHasEngine &&
|
|
@@ -416,7 +434,7 @@ function createCommandRouter(deps) {
|
|
|
416
434
|
if (!cur || !curHasEngine || shouldReattachForCwdChange) {
|
|
417
435
|
const initReason = !cur ? 'no-session' : (!curHasEngine ? 'engine-missing' : 'cwd-changed');
|
|
418
436
|
log('INFO', `SESSION-INIT [${String(sessionChatId).slice(-32)}] ${initReason}`);
|
|
419
|
-
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey,
|
|
437
|
+
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey, targetEngine);
|
|
420
438
|
}
|
|
421
439
|
}
|
|
422
440
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
|
|
4
|
+
const {
|
|
5
|
+
buildBoundSessionChatId,
|
|
6
|
+
resolveSessionRoute: _resolveSessionRoute,
|
|
7
|
+
} = require('./core/team-session-route');
|
|
4
8
|
|
|
5
9
|
function createCommandSessionResolver(deps) {
|
|
6
10
|
const {
|
|
@@ -27,11 +31,6 @@ function createCommandSessionResolver(deps) {
|
|
|
27
31
|
return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
function buildBoundSessionChatId(projectKey) {
|
|
31
|
-
const key = String(projectKey || '').trim();
|
|
32
|
-
return key ? `_bound_${key}` : '';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
34
|
function normalizeRouteCwd(cwd) {
|
|
36
35
|
if (!cwd) return null;
|
|
37
36
|
try {
|
|
@@ -48,39 +47,15 @@ function createCommandSessionResolver(deps) {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
function getSessionRoute(chatId) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
: null;
|
|
61
|
-
|
|
62
|
-
if (stickyMember) {
|
|
63
|
-
return {
|
|
64
|
-
sessionChatId: `_agent_${stickyMember.key}`,
|
|
65
|
-
cwd: normalizeRouteCwd(stickyMember.cwd || (boundProj && boundProj.cwd) || null),
|
|
66
|
-
engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (boundProj) {
|
|
71
|
-
return {
|
|
72
|
-
sessionChatId: buildBoundSessionChatId(boundKey),
|
|
73
|
-
cwd: normalizeRouteCwd(boundProj.cwd || null),
|
|
74
|
-
engine: normalizeEngineName(boundProj.engine),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const rawSession = getSession(chatId);
|
|
79
|
-
return {
|
|
80
|
-
sessionChatId: String(chatId),
|
|
81
|
-
cwd: rawSession && rawSession.cwd ? normalizeRouteCwd(rawSession.cwd) : null,
|
|
82
|
-
engine: inferStoredEngine(rawSession),
|
|
83
|
-
};
|
|
50
|
+
return _resolveSessionRoute({
|
|
51
|
+
chatId,
|
|
52
|
+
cfg: loadConfig(),
|
|
53
|
+
state: loadState(),
|
|
54
|
+
getSession,
|
|
55
|
+
normalizeCwd: normalizeRouteCwd,
|
|
56
|
+
normalizeEngineName,
|
|
57
|
+
inferStoredEngine,
|
|
58
|
+
});
|
|
84
59
|
}
|
|
85
60
|
|
|
86
61
|
function getActiveSession(chatId) {
|
|
@@ -263,7 +263,7 @@ function sanitizeQueueId(id) {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
function createMissionStartPrompt(title) {
|
|
266
|
-
return
|
|
266
|
+
return `新任务启动:"${title}"\n\nStart this mission. Read your CLAUDE.md for instructions, then decide on the first step using NEXT_DISPATCH.`;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
function loadMissionQueueState(projectKey, projectCwd, deps) {
|
|
@@ -412,7 +412,7 @@ function runCompletionHooks(projectKey, projectCwd, deps) {
|
|
|
412
412
|
deps.log('WARN', `Reactive: mission queue query failed for ${projectKey}: ${e.message}`);
|
|
413
413
|
}
|
|
414
414
|
} else if (!result.archived && fs.existsSync(scripts.missionQueue)) {
|
|
415
|
-
deps.log('WARN', `Reactive: skipping
|
|
415
|
+
deps.log('WARN', `Reactive: skipping topic pool for ${projectKey} — archive failed`);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
return result;
|
|
@@ -603,7 +603,7 @@ function generateStateFile(projectKey, config, deps) {
|
|
|
603
603
|
`round: ${round}`,
|
|
604
604
|
`last_update: "${new Date().toISOString()}"`,
|
|
605
605
|
'',
|
|
606
|
-
'
|
|
606
|
+
'history:',
|
|
607
607
|
];
|
|
608
608
|
|
|
609
609
|
for (const h of history) {
|
|
@@ -1127,8 +1127,8 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1127
1127
|
logEvent(projectKey, { type: 'ARCHIVE', path: pCwd });
|
|
1128
1128
|
}
|
|
1129
1129
|
const notifyMsg = completionResult.nextMission
|
|
1130
|
-
? `\u2705 ${pName}
|
|
1131
|
-
: `\u2705 ${pName}
|
|
1130
|
+
? `\u2705 ${pName} 完成。下一任务:${completionResult.nextMission}`
|
|
1131
|
+
: `\u2705 ${pName} 完成,无待处理任务`;
|
|
1132
1132
|
if (deps.notifyUser) deps.notifyUser(notifyMsg);
|
|
1133
1133
|
|
|
1134
1134
|
// Auto-start next mission if available — requires budget to be OK
|
|
@@ -1154,7 +1154,7 @@ function handleReactiveOutput(targetProject, output, config, deps) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
}
|
|
1156
1156
|
} else {
|
|
1157
|
-
if (deps.notifyUser) deps.notifyUser(`\u2705 ${pName}
|
|
1157
|
+
if (deps.notifyUser) deps.notifyUser(`\u2705 ${pName} 完成`);
|
|
1158
1158
|
}
|
|
1159
1159
|
return;
|
|
1160
1160
|
}
|