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.
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
- const { rawChatId: _rawChatId } = require('./core/thread-chat-id');
4
+ const {
5
+ resolveSessionRoute: _resolveSessionRoute,
6
+ resolveResumeRouteForTarget: _resolveResumeRouteForTarget,
7
+ applyResumeRouteState: _applyResumeRouteState,
8
+ } = require('./core/team-session-route');
5
9
 
6
10
  function createSessionCommandHandler(deps) {
7
11
  const {
@@ -49,45 +53,16 @@ function createSessionCommandHandler(deps) {
49
53
  return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
50
54
  }
51
55
 
52
- function buildBoundSessionChatId(projectKey) {
53
- const key = String(projectKey || '').trim();
54
- return key ? `_bound_${key}` : '';
55
- }
56
-
57
56
  function getSessionRoute(chatId) {
58
- const cfg = loadConfig();
59
- const state = loadState();
60
- const chatKey = String(chatId);
61
- const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
62
- const boundKey = agentMap[chatKey] || agentMap[_rawChatId(chatKey)] || null;
63
- const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
64
- const stickyKey = state && state.team_sticky ? (state.team_sticky[chatKey] || state.team_sticky[_rawChatId(chatKey)]) : null;
65
- const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
66
- ? boundProj.team.find((m) => m && m.key === stickyKey)
67
- : null;
68
-
69
- if (stickyMember) {
70
- return {
71
- sessionChatId: `_agent_${stickyMember.key}`,
72
- cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
73
- engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
74
- };
75
- }
76
-
77
- if (boundProj) {
78
- return {
79
- sessionChatId: buildBoundSessionChatId(boundKey),
80
- cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
81
- engine: normalizeEngineName(boundProj.engine),
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
- };
57
+ return _resolveSessionRoute({
58
+ chatId,
59
+ cfg: loadConfig(),
60
+ state: loadState(),
61
+ getSession,
62
+ normalizeCwd,
63
+ normalizeEngineName,
64
+ inferStoredEngine,
65
+ });
91
66
  }
92
67
 
93
68
  function getCurrentEngine(chatId) {
@@ -101,8 +76,8 @@ function createSessionCommandHandler(deps) {
101
76
  }
102
77
 
103
78
  // Write per-engine session slot, preserving cwd and other engine slots.
104
- function attachEngineSession(state, chatId, engine, sessionId, cwd, meta = {}) {
105
- const effectiveId = getSessionRoute(chatId).sessionChatId;
79
+ function attachEngineSession(state, chatId, engine, sessionId, cwd, meta = {}, options = {}) {
80
+ const effectiveId = options.sessionChatId || getSessionRoute(chatId).sessionChatId;
106
81
  const existing = state.sessions[effectiveId] || {};
107
82
  const existingEngines = existing.engines || {};
108
83
  const nextSlot = {
@@ -134,6 +109,17 @@ function createSessionCommandHandler(deps) {
134
109
  return null;
135
110
  }
136
111
 
112
+ function resolveResumeRouteForTarget(chatId, targetCwd, state, cfg) {
113
+ return _resolveResumeRouteForTarget({
114
+ chatId,
115
+ targetCwd,
116
+ cfg,
117
+ state,
118
+ normalizeCwd,
119
+ fallbackSessionChatId: getSessionRoute(chatId).sessionChatId,
120
+ });
121
+ }
122
+
137
123
  function resolveAttachableSession(engine, cwd, options = {}) {
138
124
  if (typeof findAttachableSession === 'function') {
139
125
  return findAttachableSession({ engine, cwd, ...options });
@@ -147,6 +133,9 @@ function createSessionCommandHandler(deps) {
147
133
 
148
134
  function attachResolvedTarget(state, chatId, engine, target, fallbackCwd) {
149
135
  const targetCwd = target && target.projectPath ? target.projectPath : fallbackCwd;
136
+ const cfg = loadConfig();
137
+ const resumeRoute = resolveResumeRouteForTarget(chatId, targetCwd, state, cfg);
138
+ _applyResumeRouteState(state, chatId, resumeRoute);
150
139
  if (target && target.pendingState) {
151
140
  attachEngineSession(state, chatId, engine, null, targetCwd, {
152
141
  started: false,
@@ -155,7 +144,7 @@ function createSessionCommandHandler(deps) {
155
144
  ...(target.sandboxMode ? { sandboxMode: target.sandboxMode } : {}),
156
145
  ...(target.approvalPolicy ? { approvalPolicy: target.approvalPolicy } : {}),
157
146
  ...(target.permissionMode ? { permissionMode: target.permissionMode } : {}),
158
- });
147
+ }, { sessionChatId: resumeRoute.sessionChatId });
159
148
  return {
160
149
  cwd: targetCwd,
161
150
  pendingState: true,
@@ -166,7 +155,7 @@ function createSessionCommandHandler(deps) {
166
155
  started: true,
167
156
  runtimeSessionObserved: true,
168
157
  clearCompactContext: true,
169
- });
158
+ }, { sessionChatId: resumeRoute.sessionChatId });
170
159
  return {
171
160
  cwd: targetCwd,
172
161
  pendingState: false,
@@ -437,6 +426,7 @@ function createSessionCommandHandler(deps) {
437
426
  const title = target.customTitle || target.summary || target.sessionId.slice(0, 8);
438
427
  const lines = [`▶️ Resumed: ${title}`];
439
428
  if (attached.cwd) lines.push(`📁 ${path.basename(attached.cwd)}`);
429
+ lines.push(`ID: ${target.sessionId}`);
440
430
  if (Array.isArray(recentDialogue) && recentDialogue.length > 0) {
441
431
  lines.push('');
442
432
  lines.push('最近对话:');
@@ -960,6 +960,7 @@ function createSessionStore(deps) {
960
960
  const proj = s.projectPath ? path.basename(s.projectPath) : '~';
961
961
  const ago = getSessionRelativeTimeLabel(s);
962
962
  const shortId = s.sessionId.slice(0, 8);
963
+ const visibleId = s.sessionId.slice(0, 18);
963
964
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
964
965
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
965
966
 
@@ -972,6 +973,7 @@ function createSessionStore(deps) {
972
973
  if (firstSnippet) line += `\n 📝 ${firstSnippet}`;
973
974
  if (lastUserSnippet && lastUserSnippet !== firstSnippet) line += `\n 💬 ${lastUserSnippet}`;
974
975
  if (lastAiSnippet) line += `\n 🤖 ${lastAiSnippet}`;
976
+ line += `\n ID ${visibleId}`;
975
977
  line += `\n /resume ${shortId}`;
976
978
  return line;
977
979
  }
@@ -984,11 +986,12 @@ function createSessionStore(deps) {
984
986
  const title = sessionDisplayTitle(s, 60, sessionTags);
985
987
  const proj = s.projectPath ? path.basename(s.projectPath) : '~';
986
988
  const ago = getSessionRelativeTimeLabel(s);
987
- const shortId = s.sessionId.slice(0, 6);
989
+ const visibleId = s.sessionId.slice(0, 18);
988
990
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
989
991
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
990
992
 
991
993
  let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago} · ${engineLabel}`;
994
+ desc += `\nID: ${visibleId}`;
992
995
  if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
993
996
  // Show first prompt, last user message, and last assistant reply
994
997
  const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
@@ -998,7 +1001,7 @@ function createSessionStore(deps) {
998
1001
  if (lastUserSnippet && lastUserSnippet !== firstSnippet) desc += `\n💬 ${lastUserSnippet}`;
999
1002
  if (lastAiSnippet) desc += `\n🤖 ${lastAiSnippet}`;
1000
1003
  elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
1001
- elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `▶️ Switch #${shortId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
1004
+ elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `▶️ Resume ${visibleId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
1002
1005
  });
1003
1006
  return elements;
1004
1007
  }
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { execSync } = require('child_process');
4
+
3
5
  /**
4
6
  * daemon-warm-pool.js
5
7
  *
@@ -20,11 +22,15 @@
20
22
  */
21
23
 
22
24
  function createWarmPool(deps) {
23
- const { log } = deps;
25
+ const {
26
+ log,
27
+ idleTimeoutMs = 5 * 60 * 1000,
28
+ hasBackgroundDescendants = defaultHasBackgroundDescendants,
29
+ } = deps;
24
30
 
25
31
  // Pool: sessionKey -> { child, sessionId, cwd, idleTimer }
26
32
  const pool = new Map();
27
- const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
33
+ const IDLE_TIMEOUT_MS = idleTimeoutMs;
28
34
 
29
35
  /**
30
36
  * Acquire a warm process for the given session key.
@@ -67,17 +73,6 @@ function createWarmPool(deps) {
67
73
  return;
68
74
  }
69
75
 
70
- // Set idle timeout
71
- const idleTimer = setTimeout(() => {
72
- const e = pool.get(sessionKey);
73
- if (e && e.child === child) {
74
- log('INFO', `[WarmPool] Idle timeout, killing warm process pid=${child.pid} for ${sessionKey}`);
75
- _killEntry(e);
76
- pool.delete(sessionKey);
77
- }
78
- }, IDLE_TIMEOUT_MS);
79
- if (typeof idleTimer.unref === 'function') idleTimer.unref();
80
-
81
76
  // Auto-cleanup on unexpected death
82
77
  const onExit = () => {
83
78
  const e = pool.get(sessionKey);
@@ -94,8 +89,9 @@ function createWarmPool(deps) {
94
89
  child,
95
90
  sessionId: meta.sessionId || '',
96
91
  cwd: meta.cwd || '',
97
- idleTimer,
92
+ idleTimer: null,
98
93
  });
94
+ _armIdleTimer(sessionKey, child);
99
95
  log('INFO', `[WarmPool] Stored warm process pid=${child.pid} for ${sessionKey} (pool size: ${pool.size})`);
100
96
  }
101
97
 
@@ -137,6 +133,34 @@ function createWarmPool(deps) {
137
133
  pool.delete(sessionKey);
138
134
  }
139
135
 
136
+ function _armIdleTimer(sessionKey, child) {
137
+ const entry = pool.get(sessionKey);
138
+ if (!entry || entry.child !== child) return;
139
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
140
+ entry.idleTimer = setTimeout(() => {
141
+ const current = pool.get(sessionKey);
142
+ if (!current || current.child !== child) return;
143
+ if (_hasLiveBackgroundDescendants(child.pid)) {
144
+ log('INFO', `[WarmPool] Idle timeout skipped for ${sessionKey}: pid=${child.pid} still has background descendants`);
145
+ _armIdleTimer(sessionKey, child);
146
+ return;
147
+ }
148
+ log('INFO', `[WarmPool] Idle timeout, killing warm process pid=${child.pid} for ${sessionKey}`);
149
+ _killEntry(current);
150
+ pool.delete(sessionKey);
151
+ }, IDLE_TIMEOUT_MS);
152
+ if (typeof entry.idleTimer.unref === 'function') entry.idleTimer.unref();
153
+ }
154
+
155
+ function _hasLiveBackgroundDescendants(pid) {
156
+ if (!Number.isFinite(pid) || pid <= 0) return false;
157
+ try {
158
+ return !!hasBackgroundDescendants(pid);
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
140
164
  /**
141
165
  * Build the stream-json user message for stdin.
142
166
  */
@@ -159,4 +183,31 @@ function createWarmPool(deps) {
159
183
  };
160
184
  }
161
185
 
186
+ function defaultHasBackgroundDescendants(pid) {
187
+ if (!Number.isFinite(pid) || pid <= 0) return false;
188
+
189
+ if (process.platform === 'win32') {
190
+ try {
191
+ const output = execSync(
192
+ `powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pid}\\").Count"`,
193
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, windowsHide: true }
194
+ ).trim();
195
+ return Number(output) > 0;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ try {
202
+ const output = execSync(`pgrep -P ${pid}`, {
203
+ encoding: 'utf8',
204
+ stdio: ['ignore', 'pipe', 'ignore'],
205
+ timeout: 3000,
206
+ }).trim();
207
+ return output.length > 0;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
162
213
  module.exports = { createWarmPool };
@@ -17,6 +17,7 @@ const MISSIONS_FILE = 'workspace/missions.md';
17
17
  const SECTIONS = ['pending', 'active', 'completed', 'abandoned'];
18
18
  const RECENT_LOG_LINES = 500;
19
19
  const ERROR_THRESHOLD = 3;
20
+ const BOOTSTRAP_MISSION_ID = 'bootstrap-001';
20
21
 
21
22
  function getMetameDir() {
22
23
  return process.env.METAME_DIR || path.join(os.homedir(), '.metame');
@@ -211,6 +212,20 @@ function completeMission(cwd, id) {
211
212
  return { success: true, topic: { id: mission.id, title: mission.title, status: 'completed' } };
212
213
  }
213
214
 
215
+ function completeBootstrapMission(cwd) {
216
+ const sections = readSections(cwd);
217
+ const found = findMission(sections, BOOTSTRAP_MISSION_ID);
218
+ if (!found || found.section !== 'active') {
219
+ return { success: false, completed: false, reason: found ? `bootstrap_${found.section}` : 'bootstrap_missing' };
220
+ }
221
+
222
+ const mission = sections.active.splice(found.index, 1)[0];
223
+ mission.status = 'completed';
224
+ sections.completed.push(mission);
225
+ writeSections(cwd, sections);
226
+ return { success: true, completed: true, topic: { id: mission.id, title: mission.title, status: 'completed' } };
227
+ }
228
+
214
229
  function listMissions(cwd) {
215
230
  const sections = readSections(cwd);
216
231
  const topics = [];
@@ -320,4 +335,12 @@ if (require.main === module) {
320
335
  process.stdout.write(JSON.stringify(result) + '\n');
321
336
  }
322
337
 
323
- module.exports = { nextMission, activateMission, completeMission, listMissions, pruneObsoleteMissions, scanLogs };
338
+ module.exports = {
339
+ nextMission,
340
+ activateMission,
341
+ completeMission,
342
+ completeBootstrapMission,
343
+ listMissions,
344
+ pruneObsoleteMissions,
345
+ scanLogs,
346
+ };
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const { execFileSync } = require('child_process');
7
7
 
8
8
  const yaml = require('./resolve-yaml');
9
- const { pruneObsoleteMissions, scanLogs } = require('./ops-mission-queue');
9
+ const { pruneObsoleteMissions, scanLogs, completeBootstrapMission } = require('./ops-mission-queue');
10
10
  const { bootstrapReactiveProject } = require('./daemon-reactive-lifecycle');
11
11
 
12
12
  const HOME = os.homedir();
@@ -52,6 +52,40 @@ function dispatchReactiveItem(item) {
52
52
  return { success: true };
53
53
  }
54
54
 
55
+ function buildScanSummary(pruned, scanned, bootstrapCompleted, bootstrap) {
56
+ const findings = [];
57
+
58
+ if ((pruned?.pruned || 0) > 0) {
59
+ findings.push(`pruned ${pruned.pruned} obsolete mission${pruned.pruned === 1 ? '' : 's'}`);
60
+ }
61
+ if ((scanned?.new_missions || 0) > 0) {
62
+ findings.push(`detected ${scanned.new_missions} new repair mission${scanned.new_missions === 1 ? '' : 's'}`);
63
+ }
64
+ if (bootstrapCompleted?.completed) {
65
+ findings.push('completed legacy bootstrap-001');
66
+ }
67
+ if (bootstrap?.started) {
68
+ const missionLabel = [bootstrap.missionId, bootstrap.mission].filter(Boolean).join(' ');
69
+ findings.push(`started repair ${missionLabel}`.trim());
70
+ }
71
+
72
+ const quiet = findings.length === 0;
73
+ const action = bootstrap?.started
74
+ ? 'repair_started'
75
+ : quiet
76
+ ? 'quiet_scan'
77
+ : 'findings_only';
78
+
79
+ return {
80
+ quiet,
81
+ action,
82
+ findings,
83
+ summary: quiet
84
+ ? 'ops-scan completed: no new recurring issues, no repair started'
85
+ : `ops-scan completed: ${findings.join('; ')}`,
86
+ };
87
+ }
88
+
55
89
  function main() {
56
90
  const config = loadConfig();
57
91
  const project = config?.projects?.[PROJECT_KEY];
@@ -63,7 +97,7 @@ function main() {
63
97
  const cwd = project.cwd.replace(/^~/, HOME);
64
98
  const pruned = pruneObsoleteMissions(cwd);
65
99
  const scanned = scanLogs(cwd);
66
-
100
+ const bootstrapCompleted = completeBootstrapMission(cwd);
67
101
  const result = bootstrapReactiveProject(PROJECT_KEY, config, {
68
102
  metameDir: METAME_DIR,
69
103
  loadState,
@@ -73,14 +107,24 @@ function main() {
73
107
  log: () => {},
74
108
  notifyUser: () => {},
75
109
  });
110
+ const summary = buildScanSummary(pruned, scanned, bootstrapCompleted, result);
76
111
 
77
112
  process.stdout.write(JSON.stringify({
78
113
  success: true,
79
114
  pruned: pruned.pruned || 0,
80
115
  new_missions: scanned.new_missions || 0,
81
116
  total_pending: scanned.total_pending || 0,
117
+ bootstrap_completed: !!bootstrapCompleted.completed,
118
+ quiet: summary.quiet,
119
+ action: summary.action,
120
+ findings: summary.findings,
121
+ summary: summary.summary,
82
122
  bootstrap: result,
83
123
  }) + '\n');
84
124
  }
85
125
 
86
126
  if (require.main === module) main();
127
+
128
+ module.exports = {
129
+ buildScanSummary,
130
+ };