metame-cli 1.5.23 → 1.5.25
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 +26 -7
- package/scripts/daemon-command-session-route.js +13 -38
- package/scripts/daemon-exec-commands.js +14 -4
- 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.25",
|
|
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,15 +425,17 @@ 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 &&
|
|
414
432
|
cur.cwd !== projCwd &&
|
|
415
433
|
!rawHasEngine;
|
|
416
|
-
|
|
434
|
+
const _isControlCmd = text && /^\/(stop|quit)$/.test(text.trim());
|
|
435
|
+
if (!_isControlCmd && (!cur || !curHasEngine || shouldReattachForCwdChange)) {
|
|
417
436
|
const initReason = !cur ? 'no-session' : (!curHasEngine ? 'engine-missing' : 'cwd-changed');
|
|
418
437
|
log('INFO', `SESSION-INIT [${String(sessionChatId).slice(-32)}] ${initReason}`);
|
|
419
|
-
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey,
|
|
438
|
+
attachOrCreateSession(sessionChatId, projCwd, proj.name || mappedKey, targetEngine);
|
|
420
439
|
}
|
|
421
440
|
}
|
|
422
441
|
|
|
@@ -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) {
|
|
@@ -4,6 +4,7 @@ const { classifyTaskUsage } = require('./usage-classifier');
|
|
|
4
4
|
const { normalizeModel } = require('./daemon-task-scheduler');
|
|
5
5
|
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
6
6
|
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
7
|
+
const { isThreadChatId: _isThreadChatId, rawChatId: _rawThreadChatId } = require('./core/thread-chat-id');
|
|
7
8
|
|
|
8
9
|
function createExecCommandHandler(deps) {
|
|
9
10
|
const {
|
|
@@ -218,7 +219,12 @@ function createExecCommandHandler(deps) {
|
|
|
218
219
|
const _pl = pipeline && pipeline.current;
|
|
219
220
|
if (_pl) {
|
|
220
221
|
_pl.clearQueue(chatId);
|
|
221
|
-
|
|
222
|
+
let stopped = _pl.interruptActive(chatId);
|
|
223
|
+
if (!stopped && _isThreadChatId(chatId)) {
|
|
224
|
+
// Thread-scoped /stop: fall back to raw chatId (task may be keyed at group level).
|
|
225
|
+
// Do NOT clearQueue(_raw) — that would discard queued tasks from other threads.
|
|
226
|
+
stopped = _pl.interruptActive(_rawThreadChatId(chatId));
|
|
227
|
+
}
|
|
222
228
|
if (stopped) {
|
|
223
229
|
await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
|
|
224
230
|
} else {
|
|
@@ -226,7 +232,8 @@ function createExecCommandHandler(deps) {
|
|
|
226
232
|
}
|
|
227
233
|
} else {
|
|
228
234
|
// Fallback: direct activeProcesses manipulation (pipeline not yet initialized)
|
|
229
|
-
const
|
|
235
|
+
const _raw = _isThreadChatId(chatId) ? _rawThreadChatId(chatId) : null;
|
|
236
|
+
const proc = activeProcesses.get(chatId) || (_raw && activeProcesses.get(_raw));
|
|
230
237
|
if (proc && proc.child) {
|
|
231
238
|
proc.aborted = true;
|
|
232
239
|
const signal = proc.killSignal || 'SIGTERM';
|
|
@@ -248,9 +255,12 @@ function createExecCommandHandler(deps) {
|
|
|
248
255
|
const _pl = pipeline && pipeline.current;
|
|
249
256
|
if (_pl) {
|
|
250
257
|
_pl.clearQueue(chatId);
|
|
251
|
-
_pl.interruptActive(chatId)
|
|
258
|
+
if (!_pl.interruptActive(chatId) && _isThreadChatId(chatId)) {
|
|
259
|
+
_pl.interruptActive(_rawThreadChatId(chatId));
|
|
260
|
+
}
|
|
252
261
|
} else {
|
|
253
|
-
const
|
|
262
|
+
const _raw = _isThreadChatId(chatId) ? _rawThreadChatId(chatId) : null;
|
|
263
|
+
const proc = activeProcesses.get(chatId) || (_raw && activeProcesses.get(_raw));
|
|
254
264
|
if (proc && proc.child) {
|
|
255
265
|
proc.aborted = true;
|
|
256
266
|
const signal = proc.killSignal || 'SIGTERM';
|