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.
Files changed (40) hide show
  1. package/index.js +157 -80
  2. package/package.json +2 -2
  3. package/scripts/bin/bootstrap-worktree.sh +20 -0
  4. package/scripts/core/audit.js +190 -0
  5. package/scripts/core/handoff.js +780 -0
  6. package/scripts/core/handoff.test.js +1074 -0
  7. package/scripts/core/memory-model.js +183 -0
  8. package/scripts/core/memory-model.test.js +486 -0
  9. package/scripts/core/reactive-paths.js +44 -0
  10. package/scripts/core/reactive-paths.test.js +35 -0
  11. package/scripts/core/reactive-prompt.js +51 -0
  12. package/scripts/core/reactive-prompt.test.js +88 -0
  13. package/scripts/core/reactive-signal.js +40 -0
  14. package/scripts/core/reactive-signal.test.js +88 -0
  15. package/scripts/core/thread-chat-id.js +52 -0
  16. package/scripts/core/thread-chat-id.test.js +113 -0
  17. package/scripts/daemon-bridges.js +92 -38
  18. package/scripts/daemon-claude-engine.js +373 -444
  19. package/scripts/daemon-command-router.js +82 -8
  20. package/scripts/daemon-engine-runtime.js +7 -10
  21. package/scripts/daemon-reactive-lifecycle.js +100 -33
  22. package/scripts/daemon-session-commands.js +133 -43
  23. package/scripts/daemon-session-store.js +300 -82
  24. package/scripts/daemon-team-dispatch.js +16 -16
  25. package/scripts/daemon.js +21 -175
  26. package/scripts/deploy-manifest.js +90 -0
  27. package/scripts/docs/maintenance-manual.md +14 -11
  28. package/scripts/docs/pointer-map.md +13 -4
  29. package/scripts/feishu-adapter.js +31 -27
  30. package/scripts/hooks/intent-engine.js +6 -3
  31. package/scripts/hooks/intent-memory-recall.js +1 -0
  32. package/scripts/hooks/intent-perpetual.js +1 -1
  33. package/scripts/memory-extract.js +5 -97
  34. package/scripts/memory-gc.js +35 -90
  35. package/scripts/memory-migrate-v2.js +304 -0
  36. package/scripts/memory-nightly-reflect.js +40 -41
  37. package/scripts/memory.js +340 -859
  38. package/scripts/migrate-reactive-paths.js +122 -0
  39. package/scripts/signal-capture.js +4 -0
  40. 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]: { id: sessionId, started: true } },
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
- async function autoCreateSessionWhenEmpty(bot, chatId, cwd, engine) {
122
- const resolvedCwd = cwd ? normalizeCwd(cwd) : null;
123
- if (!resolvedCwd || !fs.existsSync(resolvedCwd)) {
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 route = getSessionRoute(chatId);
128
- const session = createSession(route.sessionChatId, resolvedCwd, '', engine || getDefaultEngine());
129
- await bot.sendMessage(chatId, `📁 ${path.basename(resolvedCwd)}\n✅ 已自动创建新会话\nWorkdir: ${session.cwd}`);
130
- return true;
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
- let s = null;
239
- if (curCwd) {
240
- const cwdSessions = listRecentSessions(1, curCwd, currentEngine);
241
- if (cwdSessions.length > 0) s = cwdSessions[0];
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
- attachEngineSession(state2, chatId, engineByCwd, s.sessionId, s.projectPath || HOME);
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
- return autoCreateSessionWhenEmpty(
330
- bot,
331
- chatId,
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
- let candidates = filtered;
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
- attachEngineSession(state2, chatId, engineByCwd, target.sessionId, target.projectPath);
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)}`);