metame-cli 1.5.22 → 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.
@@ -263,7 +263,7 @@ function sanitizeQueueId(id) {
263
263
  }
264
264
 
265
265
  function createMissionStartPrompt(title) {
266
- return `New mission: "${title}"\n\nStart this mission. Read your CLAUDE.md for instructions, then decide on the first step using NEXT_DISPATCH.`;
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 mission queue for ${projectKey} — archive did not succeed`);
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
- '# Phase history (from event log)',
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} mission completed. Next: ${completionResult.nextMission}`
1131
- : `\u2705 ${pName} mission completed. No pending missions — entering idle.`;
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} mission completed`);
1157
+ if (deps.notifyUser) deps.notifyUser(`\u2705 ${pName} 完成`);
1158
1158
  }
1159
1159
  return;
1160
1160
  }
@@ -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 {
@@ -30,6 +34,7 @@ function createSessionCommandHandler(deps) {
30
34
  sessionRichLabel,
31
35
  buildSessionCardElements,
32
36
  getSessionRecentContext,
37
+ getSessionRecentDialogue,
33
38
  getDefaultEngine = () => 'claude',
34
39
  } = deps;
35
40
 
@@ -48,45 +53,16 @@ function createSessionCommandHandler(deps) {
48
53
  return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
49
54
  }
50
55
 
51
- function buildBoundSessionChatId(projectKey) {
52
- const key = String(projectKey || '').trim();
53
- return key ? `_bound_${key}` : '';
54
- }
55
-
56
56
  function getSessionRoute(chatId) {
57
- const cfg = loadConfig();
58
- const state = loadState();
59
- const chatKey = String(chatId);
60
- const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
61
- const boundKey = agentMap[chatKey] || agentMap[_rawChatId(chatKey)] || null;
62
- const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
63
- const stickyKey = state && state.team_sticky ? (state.team_sticky[chatKey] || state.team_sticky[_rawChatId(chatKey)]) : null;
64
- const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
65
- ? boundProj.team.find((m) => m && m.key === stickyKey)
66
- : null;
67
-
68
- if (stickyMember) {
69
- return {
70
- sessionChatId: `_agent_${stickyMember.key}`,
71
- cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
72
- engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
73
- };
74
- }
75
-
76
- if (boundProj) {
77
- return {
78
- sessionChatId: buildBoundSessionChatId(boundKey),
79
- cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
80
- engine: normalizeEngineName(boundProj.engine),
81
- };
82
- }
83
-
84
- const rawSession = getSession(chatId);
85
- return {
86
- sessionChatId: String(chatId),
87
- cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
88
- engine: inferStoredEngine(rawSession),
89
- };
57
+ return _resolveSessionRoute({
58
+ chatId,
59
+ cfg: loadConfig(),
60
+ state: loadState(),
61
+ getSession,
62
+ normalizeCwd,
63
+ normalizeEngineName,
64
+ inferStoredEngine,
65
+ });
90
66
  }
91
67
 
92
68
  function getCurrentEngine(chatId) {
@@ -100,8 +76,8 @@ function createSessionCommandHandler(deps) {
100
76
  }
101
77
 
102
78
  // Write per-engine session slot, preserving cwd and other engine slots.
103
- function attachEngineSession(state, chatId, engine, sessionId, cwd, meta = {}) {
104
- const effectiveId = getSessionRoute(chatId).sessionChatId;
79
+ function attachEngineSession(state, chatId, engine, sessionId, cwd, meta = {}, options = {}) {
80
+ const effectiveId = options.sessionChatId || getSessionRoute(chatId).sessionChatId;
105
81
  const existing = state.sessions[effectiveId] || {};
106
82
  const existingEngines = existing.engines || {};
107
83
  const nextSlot = {
@@ -133,6 +109,17 @@ function createSessionCommandHandler(deps) {
133
109
  return null;
134
110
  }
135
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
+
136
123
  function resolveAttachableSession(engine, cwd, options = {}) {
137
124
  if (typeof findAttachableSession === 'function') {
138
125
  return findAttachableSession({ engine, cwd, ...options });
@@ -146,6 +133,9 @@ function createSessionCommandHandler(deps) {
146
133
 
147
134
  function attachResolvedTarget(state, chatId, engine, target, fallbackCwd) {
148
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);
149
139
  if (target && target.pendingState) {
150
140
  attachEngineSession(state, chatId, engine, null, targetCwd, {
151
141
  started: false,
@@ -154,7 +144,7 @@ function createSessionCommandHandler(deps) {
154
144
  ...(target.sandboxMode ? { sandboxMode: target.sandboxMode } : {}),
155
145
  ...(target.approvalPolicy ? { approvalPolicy: target.approvalPolicy } : {}),
156
146
  ...(target.permissionMode ? { permissionMode: target.permissionMode } : {}),
157
- });
147
+ }, { sessionChatId: resumeRoute.sessionChatId });
158
148
  return {
159
149
  cwd: targetCwd,
160
150
  pendingState: true,
@@ -165,7 +155,7 @@ function createSessionCommandHandler(deps) {
165
155
  started: true,
166
156
  runtimeSessionObserved: true,
167
157
  clearCompactContext: true,
168
- });
158
+ }, { sessionChatId: resumeRoute.sessionChatId });
169
159
  return {
170
160
  cwd: targetCwd,
171
161
  pendingState: false,
@@ -430,11 +420,25 @@ function createSessionCommandHandler(deps) {
430
420
  const recentCtx = typeof getSessionRecentContext === 'function'
431
421
  ? getSessionRecentContext(target.sessionId)
432
422
  : null;
423
+ const recentDialogue = typeof getSessionRecentDialogue === 'function'
424
+ ? getSessionRecentDialogue(target.sessionId, 4)
425
+ : null;
433
426
  const title = target.customTitle || target.summary || target.sessionId.slice(0, 8);
434
427
  const lines = [`▶️ Resumed: ${title}`];
435
428
  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)}`);
429
+ lines.push(`ID: ${target.sessionId}`);
430
+ if (Array.isArray(recentDialogue) && recentDialogue.length > 0) {
431
+ lines.push('');
432
+ lines.push('最近对话:');
433
+ recentDialogue.forEach((item) => {
434
+ const marker = item.role === 'assistant' ? '🤖' : '👤';
435
+ const text = String(item.text || '').replace(/\n/g, ' ').slice(0, 120);
436
+ if (text) lines.push(`${marker} ${text}`);
437
+ });
438
+ } else {
439
+ if (recentCtx && recentCtx.lastUser) lines.push(`👤 ${String(recentCtx.lastUser).replace(/\n/g, ' ').slice(0, 80)}`);
440
+ if (recentCtx && recentCtx.lastAssistant) lines.push(`🤖 ${String(recentCtx.lastAssistant).replace(/\n/g, ' ').slice(0, 80)}`);
441
+ }
438
442
  await bot.sendMessage(chatId, lines.join('\n'));
439
443
  return true;
440
444
  }
@@ -352,6 +352,29 @@ function createSessionStore(deps) {
352
352
  return '';
353
353
  }
354
354
 
355
+ function extractRecentClaudeDialogueFromLines(lines, maxMessages = 4) {
356
+ const collected = [];
357
+ for (const line of lines) {
358
+ if (!line) continue;
359
+ try {
360
+ const d = JSON.parse(line);
361
+ if (d.type === 'user' && d.message && d.userType !== 'internal') {
362
+ const raw = _extractMessageText(d);
363
+ if (raw.length > 2) {
364
+ collected.push({ role: 'user', text: raw.slice(0, 160) });
365
+ }
366
+ } else if (d.type === 'assistant' && d.message) {
367
+ const raw = _extractMessageText(d);
368
+ if (raw.length > 2) {
369
+ collected.push({ role: 'assistant', text: raw.slice(0, 160) });
370
+ }
371
+ }
372
+ } catch { /* skip */ }
373
+ if (collected.length >= maxMessages) break;
374
+ }
375
+ return collected.reverse();
376
+ }
377
+
355
378
  function scanClaudeSessions() {
356
379
  try {
357
380
  if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return [];
@@ -638,6 +661,59 @@ function createSessionStore(deps) {
638
661
  }
639
662
  }
640
663
 
664
+ function parseCodexSessionRecentDialogue(sessionFile, maxMessages = 4) {
665
+ try {
666
+ if (!sessionFile || !fs.existsSync(sessionFile)) return [];
667
+ const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
668
+ const items = [];
669
+ let pendingAssistant = '';
670
+
671
+ function pushDialogueItem(role, text) {
672
+ const clean = String(text || '').trim();
673
+ if (!clean) return;
674
+ const clipped = clean.slice(0, 160);
675
+ const prev = items[items.length - 1];
676
+ if (prev && prev.role === role) {
677
+ prev.text = clipped;
678
+ return;
679
+ }
680
+ items.push({ role, text: clipped });
681
+ }
682
+
683
+ for (const line of lines) {
684
+ let entry;
685
+ try {
686
+ entry = JSON.parse(line);
687
+ } catch {
688
+ continue;
689
+ }
690
+ if (entry.type === 'response_item' && entry.payload && entry.payload.type === 'message') {
691
+ const role = String(entry.payload.role || '').toLowerCase();
692
+ if (role !== 'user' && role !== 'assistant') continue;
693
+ const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.content || entry.payload));
694
+ if (!text) continue;
695
+ if (role === 'user') {
696
+ if (pendingAssistant) {
697
+ pushDialogueItem('assistant', pendingAssistant);
698
+ pendingAssistant = '';
699
+ }
700
+ pushDialogueItem('user', text);
701
+ } else {
702
+ pendingAssistant = '';
703
+ pushDialogueItem('assistant', text);
704
+ }
705
+ } else if (entry.type === 'event_msg' && entry.payload && entry.payload.type === 'agent_message') {
706
+ const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.message));
707
+ if (text) pendingAssistant = text;
708
+ }
709
+ }
710
+ if (pendingAssistant) pushDialogueItem('assistant', pendingAssistant);
711
+ return items.slice(-maxMessages);
712
+ } catch {
713
+ return [];
714
+ }
715
+ }
716
+
641
717
  function enrichCodexSession(session) {
642
718
  if (!session || session._enriched) return session;
643
719
  try {
@@ -884,6 +960,7 @@ function createSessionStore(deps) {
884
960
  const proj = s.projectPath ? path.basename(s.projectPath) : '~';
885
961
  const ago = getSessionRelativeTimeLabel(s);
886
962
  const shortId = s.sessionId.slice(0, 8);
963
+ const visibleId = s.sessionId.slice(0, 18);
887
964
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
888
965
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
889
966
 
@@ -896,6 +973,7 @@ function createSessionStore(deps) {
896
973
  if (firstSnippet) line += `\n 📝 ${firstSnippet}`;
897
974
  if (lastUserSnippet && lastUserSnippet !== firstSnippet) line += `\n 💬 ${lastUserSnippet}`;
898
975
  if (lastAiSnippet) line += `\n 🤖 ${lastAiSnippet}`;
976
+ line += `\n ID ${visibleId}`;
899
977
  line += `\n /resume ${shortId}`;
900
978
  return line;
901
979
  }
@@ -908,11 +986,12 @@ function createSessionStore(deps) {
908
986
  const title = sessionDisplayTitle(s, 60, sessionTags);
909
987
  const proj = s.projectPath ? path.basename(s.projectPath) : '~';
910
988
  const ago = getSessionRelativeTimeLabel(s);
911
- const shortId = s.sessionId.slice(0, 6);
989
+ const visibleId = s.sessionId.slice(0, 18);
912
990
  const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
913
991
  const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
914
992
 
915
993
  let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago} · ${engineLabel}`;
994
+ desc += `\nID: ${visibleId}`;
916
995
  if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
917
996
  // Show first prompt, last user message, and last assistant reply
918
997
  const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
@@ -922,7 +1001,7 @@ function createSessionStore(deps) {
922
1001
  if (lastUserSnippet && lastUserSnippet !== firstSnippet) desc += `\n💬 ${lastUserSnippet}`;
923
1002
  if (lastAiSnippet) desc += `\n🤖 ${lastAiSnippet}`;
924
1003
  elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
925
- 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}` } }] });
926
1005
  });
927
1006
  return elements;
928
1007
  }
@@ -1173,6 +1252,32 @@ function createSessionStore(deps) {
1173
1252
  } catch { return null; }
1174
1253
  }
1175
1254
 
1255
+ function getSessionRecentDialogue(sessionId, maxMessages = 4) {
1256
+ try {
1257
+ const limit = Math.max(1, Math.min(Number(maxMessages) || 4, 8));
1258
+ const sessionFile = findSessionFile(sessionId);
1259
+ if (sessionFile) {
1260
+ const stat = fs.statSync(sessionFile);
1261
+ const tailSize = Math.min(262144, stat.size);
1262
+ const buf = Buffer.alloc(tailSize);
1263
+ const fd = fs.openSync(sessionFile, 'r');
1264
+ try {
1265
+ fs.readSync(fd, buf, 0, tailSize, stat.size - tailSize);
1266
+ } finally {
1267
+ fs.closeSync(fd);
1268
+ }
1269
+ const lines = buf.toString('utf8').split('\n').reverse();
1270
+ const dialogue = extractRecentClaudeDialogueFromLines(lines, limit);
1271
+ return dialogue.length ? dialogue : null;
1272
+ }
1273
+ const codexFile = findCodexSessionFile(sessionId);
1274
+ const dialogue = parseCodexSessionRecentDialogue(codexFile, limit);
1275
+ return dialogue.length ? dialogue : null;
1276
+ } catch {
1277
+ return null;
1278
+ }
1279
+ }
1280
+
1176
1281
  function markSessionStarted(chatId, engine) {
1177
1282
  const state = loadState();
1178
1283
  const s = state.sessions[chatId];
@@ -1368,6 +1473,7 @@ function createSessionStore(deps) {
1368
1473
  writeSessionName,
1369
1474
  markSessionStarted,
1370
1475
  getSessionRecentContext,
1476
+ getSessionRecentDialogue,
1371
1477
  isEngineSessionValid,
1372
1478
  getCodexSessionSandboxProfile,
1373
1479
  getCodexSessionPermissionMode,
@@ -1378,6 +1484,7 @@ function createSessionStore(deps) {
1378
1484
  stripCodexInjectedHints,
1379
1485
  looksLikeInternalCodexPrompt,
1380
1486
  parseCodexSessionPreview,
1487
+ parseCodexSessionRecentDialogue,
1381
1488
  buildPendingStateSessions,
1382
1489
  },
1383
1490
  };
@@ -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 };
package/scripts/daemon.js CHANGED
@@ -36,9 +36,11 @@ const fs = require('fs');
36
36
  const path = require('path');
37
37
  const os = require('os');
38
38
  const { execSync, execFileSync, execFile, spawn } = require('child_process');
39
+ const { bootstrapRuntimeModulePaths } = require('./runtime-bootstrap');
39
40
 
40
41
  const HOME = os.homedir();
41
42
  const METAME_DIR = path.join(HOME, '.metame');
43
+ bootstrapRuntimeModulePaths(METAME_DIR);
42
44
  const CONFIG_FILE = path.join(METAME_DIR, 'daemon.yaml');
43
45
  const STATE_FILE = path.join(METAME_DIR, 'daemon_state.json');
44
46
  const PID_FILE = path.join(METAME_DIR, 'daemon.pid');
@@ -1765,6 +1767,7 @@ const {
1765
1767
  sessionLabel,
1766
1768
  sessionRichLabel,
1767
1769
  getSessionRecentContext,
1770
+ getSessionRecentDialogue,
1768
1771
  buildSessionCardElements,
1769
1772
  getSession,
1770
1773
  getSessionForEngine,
@@ -2014,6 +2017,8 @@ const { handleSessionCommand } = createSessionCommandHandler({
2014
2017
  loadSessionTags,
2015
2018
  sessionRichLabel,
2016
2019
  buildSessionCardElements,
2020
+ getSessionRecentContext,
2021
+ getSessionRecentDialogue,
2017
2022
  sessionLabel,
2018
2023
  getDefaultEngine,
2019
2024
  });
@@ -2051,6 +2056,7 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
2051
2056
  findSessionFile,
2052
2057
  listRecentSessions,
2053
2058
  getSessionRecentContext,
2059
+ getSessionRecentDialogue,
2054
2060
  isEngineSessionValid,
2055
2061
  getCodexSessionSandboxProfile,
2056
2062
  getCodexSessionPermissionMode,
@@ -2122,6 +2128,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
2122
2128
  loadSessionTags,
2123
2129
  sessionRichLabel,
2124
2130
  getSessionRecentContext,
2131
+ getSessionRecentDialogue,
2125
2132
  pendingBinds,
2126
2133
  pendingAgentFlows,
2127
2134
  pendingTeamFlows,
@@ -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
+ };
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
+ const { bootstrapRuntimeModulePaths } = require('./runtime-bootstrap');
5
+
6
+ bootstrapRuntimeModulePaths(__dirname);
4
7
 
5
8
  let yaml;
6
9
  try {