metame-cli 1.5.19 → 1.5.21
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 +92 -38
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
|
|
4
|
+
const { rawChatId: _rawChatId } = require('./core/thread-chat-id');
|
|
4
5
|
|
|
5
6
|
function createSessionCommandHandler(deps) {
|
|
6
7
|
const {
|
|
@@ -19,6 +20,7 @@ function createSessionCommandHandler(deps) {
|
|
|
19
20
|
getCachedFile,
|
|
20
21
|
getSession,
|
|
21
22
|
listRecentSessions,
|
|
23
|
+
findAttachableSession,
|
|
22
24
|
getSessionFileMtime,
|
|
23
25
|
formatRelativeTime,
|
|
24
26
|
sendDirListing,
|
|
@@ -27,6 +29,7 @@ function createSessionCommandHandler(deps) {
|
|
|
27
29
|
loadSessionTags,
|
|
28
30
|
sessionRichLabel,
|
|
29
31
|
buildSessionCardElements,
|
|
32
|
+
getSessionRecentContext,
|
|
30
33
|
getDefaultEngine = () => 'claude',
|
|
31
34
|
} = deps;
|
|
32
35
|
|
|
@@ -55,9 +58,9 @@ function createSessionCommandHandler(deps) {
|
|
|
55
58
|
const state = loadState();
|
|
56
59
|
const chatKey = String(chatId);
|
|
57
60
|
const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
58
|
-
const boundKey = agentMap[chatKey] || null;
|
|
61
|
+
const boundKey = agentMap[chatKey] || agentMap[_rawChatId(chatKey)] || null;
|
|
59
62
|
const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
|
|
60
|
-
const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
|
|
63
|
+
const stickyKey = state && state.team_sticky ? (state.team_sticky[chatKey] || state.team_sticky[_rawChatId(chatKey)]) : null;
|
|
61
64
|
const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
|
|
62
65
|
? boundProj.team.find((m) => m && m.key === stickyKey)
|
|
63
66
|
: null;
|
|
@@ -97,14 +100,26 @@ function createSessionCommandHandler(deps) {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
// Write per-engine session slot, preserving cwd and other engine slots.
|
|
100
|
-
function attachEngineSession(state, chatId, engine, sessionId, cwd) {
|
|
103
|
+
function attachEngineSession(state, chatId, engine, sessionId, cwd, meta = {}) {
|
|
101
104
|
const effectiveId = getSessionRoute(chatId).sessionChatId;
|
|
102
105
|
const existing = state.sessions[effectiveId] || {};
|
|
103
106
|
const existingEngines = existing.engines || {};
|
|
107
|
+
const nextSlot = {
|
|
108
|
+
...(existingEngines[engine] || {}),
|
|
109
|
+
...(sessionId ? { id: sessionId } : { id: null }),
|
|
110
|
+
started: meta.started !== false,
|
|
111
|
+
};
|
|
112
|
+
if (meta.compactContext) nextSlot.compactContext = String(meta.compactContext);
|
|
113
|
+
else if (meta.clearCompactContext) delete nextSlot.compactContext;
|
|
114
|
+
if (meta.runtimeSessionObserved === false) nextSlot.runtimeSessionObserved = false;
|
|
115
|
+
else if (meta.runtimeSessionObserved === true) nextSlot.runtimeSessionObserved = true;
|
|
116
|
+
if (meta.sandboxMode) nextSlot.sandboxMode = meta.sandboxMode;
|
|
117
|
+
if (meta.approvalPolicy) nextSlot.approvalPolicy = meta.approvalPolicy;
|
|
118
|
+
if (meta.permissionMode) nextSlot.permissionMode = meta.permissionMode;
|
|
104
119
|
state.sessions[effectiveId] = {
|
|
105
120
|
...existing,
|
|
106
121
|
cwd: cwd || existing.cwd || HOME,
|
|
107
|
-
engines: { ...existingEngines, [engine]:
|
|
122
|
+
engines: { ...existingEngines, [engine]: nextSlot },
|
|
108
123
|
};
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -118,16 +133,52 @@ function createSessionCommandHandler(deps) {
|
|
|
118
133
|
return null;
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await bot.sendMessage(chatId, `No sessions found${resolvedCwd ? ' in ' + path.basename(resolvedCwd) : ''}. Try /new first.`);
|
|
125
|
-
return true;
|
|
136
|
+
function resolveAttachableSession(engine, cwd, options = {}) {
|
|
137
|
+
if (typeof findAttachableSession === 'function') {
|
|
138
|
+
return findAttachableSession({ engine, cwd, ...options });
|
|
126
139
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
const matches = listRecentSessions(options.limit || 10, cwd || null, engine);
|
|
141
|
+
if (matches.length > 0) return matches[0];
|
|
142
|
+
if (!options.allowGlobalFallback) return null;
|
|
143
|
+
const global = listRecentSessions(options.limit || 10, null, engine);
|
|
144
|
+
return global[0] || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function attachResolvedTarget(state, chatId, engine, target, fallbackCwd) {
|
|
148
|
+
const targetCwd = target && target.projectPath ? target.projectPath : fallbackCwd;
|
|
149
|
+
if (target && target.pendingState) {
|
|
150
|
+
attachEngineSession(state, chatId, engine, null, targetCwd, {
|
|
151
|
+
started: false,
|
|
152
|
+
compactContext: target.compactContext || '',
|
|
153
|
+
runtimeSessionObserved: false,
|
|
154
|
+
...(target.sandboxMode ? { sandboxMode: target.sandboxMode } : {}),
|
|
155
|
+
...(target.approvalPolicy ? { approvalPolicy: target.approvalPolicy } : {}),
|
|
156
|
+
...(target.permissionMode ? { permissionMode: target.permissionMode } : {}),
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
cwd: targetCwd,
|
|
160
|
+
pendingState: true,
|
|
161
|
+
label: target.customTitle || target.summary || '待接续上下文',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
attachEngineSession(state, chatId, engine, target && target.sessionId ? target.sessionId : null, targetCwd, {
|
|
165
|
+
started: true,
|
|
166
|
+
runtimeSessionObserved: true,
|
|
167
|
+
clearCompactContext: true,
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
cwd: targetCwd,
|
|
171
|
+
pendingState: false,
|
|
172
|
+
label: target && (target.customTitle || target.summary || target.sessionId) ? (target.customTitle || target.summary || target.sessionId) : 'Session',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listResumeCandidates(chatId, limit = 15) {
|
|
177
|
+
const boundCwd = getBoundCwd(chatId);
|
|
178
|
+
const currentEngine = getCurrentEngine(chatId);
|
|
179
|
+
const local = listRecentSessions(limit, boundCwd, currentEngine);
|
|
180
|
+
if (local && local.length > 0) return local;
|
|
181
|
+
return listRecentSessions(limit, null, currentEngine);
|
|
131
182
|
}
|
|
132
183
|
|
|
133
184
|
async function handleSessionCommand(ctx) {
|
|
@@ -235,15 +286,10 @@ function createSessionCommandHandler(deps) {
|
|
|
235
286
|
const curCwd = curSession ? curSession.cwd : null;
|
|
236
287
|
|
|
237
288
|
// Strategy: try current cwd first, then fall back to global
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
if (!s) {
|
|
244
|
-
const globalSessions = listRecentSessions(1, null, currentEngine);
|
|
245
|
-
if (globalSessions.length > 0) s = globalSessions[0];
|
|
246
|
-
}
|
|
289
|
+
const s = resolveAttachableSession(currentEngine, curCwd, {
|
|
290
|
+
allowGlobalFallback: true,
|
|
291
|
+
preferredSessionId: curSession && curSession.id,
|
|
292
|
+
});
|
|
247
293
|
|
|
248
294
|
if (!s) {
|
|
249
295
|
// Last resort: use __continue__ to resume whatever Claude thinks is last
|
|
@@ -259,8 +305,12 @@ function createSessionCommandHandler(deps) {
|
|
|
259
305
|
const state2 = loadState();
|
|
260
306
|
const cfgForEngine = loadConfig();
|
|
261
307
|
const engineByCwd = normalizeEngineName(s.engine) || inferEngineByCwd(cfgForEngine, s.projectPath || HOME) || getDefaultEngine();
|
|
262
|
-
|
|
308
|
+
const attached = attachResolvedTarget(state2, chatId, engineByCwd, s, s.projectPath || HOME);
|
|
263
309
|
saveState(state2);
|
|
310
|
+
if (attached.pendingState) {
|
|
311
|
+
await bot.sendMessage(chatId, `⚡ 已接入待接续上下文\n📁 ${path.basename(attached.cwd || curCwd || HOME)}`);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
264
314
|
// Display: name/summary + id on separate lines
|
|
265
315
|
const name = s.customTitle;
|
|
266
316
|
const shortId = s.sessionId.slice(0, 8);
|
|
@@ -326,12 +376,9 @@ function createSessionCommandHandler(deps) {
|
|
|
326
376
|
if (text === '/sessions') {
|
|
327
377
|
const allSessions = listRecentSessions(15, getBoundCwd(chatId), getCurrentEngine(chatId));
|
|
328
378
|
if (allSessions.length === 0) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
getBoundCwd(chatId) || (getSession(chatId) && getSession(chatId).cwd),
|
|
333
|
-
getCurrentEngine(chatId)
|
|
334
|
-
);
|
|
379
|
+
const resolvedCwd = getBoundCwd(chatId) || (getSession(chatId) && getSession(chatId).cwd);
|
|
380
|
+
await bot.sendMessage(chatId, `No sessions found${resolvedCwd ? ' in ' + path.basename(resolvedCwd) : ''}. Try /new first.`);
|
|
381
|
+
return true;
|
|
335
382
|
}
|
|
336
383
|
if (bot.sendButtons) {
|
|
337
384
|
await bot.sendRawCard(chatId, '📋 Recent Sessions', buildSessionCardElements(allSessions));
|
|
@@ -346,6 +393,52 @@ function createSessionCommandHandler(deps) {
|
|
|
346
393
|
return true;
|
|
347
394
|
}
|
|
348
395
|
|
|
396
|
+
// /resume [id] — show recent sessions or attach a selected one
|
|
397
|
+
if (text === '/resume' || text.startsWith('/resume ')) {
|
|
398
|
+
const arg = text.slice(7).trim();
|
|
399
|
+
const allSessions = listResumeCandidates(chatId, arg ? 50 : 15);
|
|
400
|
+
|
|
401
|
+
if (!arg) {
|
|
402
|
+
if (allSessions.length === 0) {
|
|
403
|
+
await bot.sendMessage(chatId, 'No sessions found. Try /new first.');
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (bot.sendRawCard && (bot.sendButtons || bot.sendCard)) {
|
|
407
|
+
await bot.sendRawCard(chatId, '📋 Resume Session', buildSessionCardElements(allSessions));
|
|
408
|
+
} else {
|
|
409
|
+
const sessionTags = loadSessionTags();
|
|
410
|
+
let msg = '📋 Resume Session\n\n';
|
|
411
|
+
allSessions.forEach((s, i) => {
|
|
412
|
+
msg += sessionRichLabel(s, i + 1, sessionTags) + '\n';
|
|
413
|
+
});
|
|
414
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const target = allSessions.find((s) => s.sessionId === arg || s.sessionId.startsWith(arg));
|
|
420
|
+
if (!target) {
|
|
421
|
+
await bot.sendMessage(chatId, `Session not found: ${arg.slice(0, 8)}`);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const state2 = loadState();
|
|
426
|
+
const targetEngine = normalizeEngineName(target.engine) || getCurrentEngine(chatId);
|
|
427
|
+
const attached = attachResolvedTarget(state2, chatId, targetEngine, target, target.projectPath || HOME);
|
|
428
|
+
saveState(state2);
|
|
429
|
+
|
|
430
|
+
const recentCtx = typeof getSessionRecentContext === 'function'
|
|
431
|
+
? getSessionRecentContext(target.sessionId)
|
|
432
|
+
: null;
|
|
433
|
+
const title = target.customTitle || target.summary || target.sessionId.slice(0, 8);
|
|
434
|
+
const lines = [`▶️ Resumed: ${title}`];
|
|
435
|
+
if (attached.cwd) lines.push(`📁 ${path.basename(attached.cwd)}`);
|
|
436
|
+
if (recentCtx && recentCtx.lastUser) lines.push(`👤 ${String(recentCtx.lastUser).replace(/\n/g, ' ').slice(0, 80)}`);
|
|
437
|
+
if (recentCtx && recentCtx.lastAssistant) lines.push(`🤖 ${String(recentCtx.lastAssistant).replace(/\n/g, ' ').slice(0, 80)}`);
|
|
438
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
349
442
|
// /sess <id> — show session detail card with switch button
|
|
350
443
|
if (text.startsWith('/sess ')) {
|
|
351
444
|
const sid = text.slice(6).trim();
|
|
@@ -430,27 +523,24 @@ function createSessionCommandHandler(deps) {
|
|
|
430
523
|
const route = getSessionRoute(chatId);
|
|
431
524
|
const currentSession = getSession(route.sessionChatId) || getSession(chatId);
|
|
432
525
|
const excludeId = currentSession?.id;
|
|
433
|
-
const recent = listRecentSessions(10, null, getCurrentEngine(chatId));
|
|
434
|
-
const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
|
|
435
|
-
|
|
436
526
|
// For bound chats, prefer sessions from the same project to avoid
|
|
437
527
|
// the bound-chat guard (handleCommand) immediately overwriting with a new session.
|
|
438
528
|
const boundCwd = getBoundCwd(chatId);
|
|
529
|
+
const target = resolveAttachableSession(getCurrentEngine(chatId), boundCwd, {
|
|
530
|
+
allowGlobalFallback: true,
|
|
531
|
+
excludeSessionId: excludeId,
|
|
532
|
+
});
|
|
439
533
|
|
|
440
|
-
|
|
441
|
-
if (boundCwd) {
|
|
442
|
-
const boundFiltered = filtered.filter(s => s.projectPath && normalizeCwd(s.projectPath) === boundCwd);
|
|
443
|
-
if (boundFiltered.length > 0) candidates = boundFiltered;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (candidates.length > 0 && candidates[0].projectPath) {
|
|
447
|
-
const target = candidates[0];
|
|
448
|
-
// Switch to that session (like /resume) AND its directory
|
|
534
|
+
if (target && target.projectPath) {
|
|
449
535
|
const state2 = loadState();
|
|
450
536
|
const cfgForEngine = loadConfig();
|
|
451
|
-
const engineByCwd = inferEngineByCwd(cfgForEngine, target.projectPath) || getDefaultEngine();
|
|
452
|
-
|
|
537
|
+
const engineByCwd = normalizeEngineName(target.engine) || inferEngineByCwd(cfgForEngine, target.projectPath) || getDefaultEngine();
|
|
538
|
+
const attached = attachResolvedTarget(state2, chatId, engineByCwd, target, target.projectPath);
|
|
453
539
|
saveState(state2);
|
|
540
|
+
if (attached.pendingState) {
|
|
541
|
+
await bot.sendMessage(chatId, `🔄 Synced pending context\n📁 ${path.basename(attached.cwd || HOME)}`);
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
454
544
|
const name = target.customTitle || target.summary || '';
|
|
455
545
|
const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
|
|
456
546
|
await bot.sendMessage(chatId, `🔄 Synced to: ${label}\n📁 ${path.basename(target.projectPath)}`);
|