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.
- package/index.js +18 -1
- package/package.json +3 -2
- package/scripts/core/team-session-route.js +164 -0
- package/scripts/daemon-agent-commands.js +41 -43
- package/scripts/daemon-bridges.js +50 -9
- package/scripts/daemon-claude-engine.js +13 -1
- package/scripts/daemon-command-router.js +24 -6
- package/scripts/daemon-command-session-route.js +13 -38
- package/scripts/daemon-reactive-lifecycle.js +6 -6
- package/scripts/daemon-session-commands.js +49 -45
- package/scripts/daemon-session-store.js +109 -2
- package/scripts/daemon-warm-pool.js +65 -14
- package/scripts/daemon.js +7 -0
- package/scripts/ops-mission-queue.js +24 -1
- package/scripts/ops-reactive-bootstrap.js +46 -2
- package/scripts/resolve-yaml.js +3 -0
- package/scripts/runtime-bootstrap.js +77 -0
- 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
|
@@ -263,7 +263,7 @@ function sanitizeQueueId(id) {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
function createMissionStartPrompt(title) {
|
|
266
|
-
return
|
|
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
|
|
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
|
-
'
|
|
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}
|
|
1131
|
-
: `\u2705 ${pName}
|
|
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}
|
|
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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
437
|
-
if (
|
|
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
|
|
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: `▶️
|
|
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 {
|
|
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 =
|
|
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 = {
|
|
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
|
+
};
|