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
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function resolveNodeEntry(fs, path, cmdPath) {
|
|
4
|
+
try {
|
|
5
|
+
const content = fs.readFileSync(cmdPath, 'utf8');
|
|
6
|
+
const match = content.match(/"([^"]+\.js)"\s*%\*\s*$/m);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const entry = match[1].replace(/%dp0%/gi, path.dirname(cmdPath) + path.sep);
|
|
9
|
+
return fs.existsSync(entry) ? entry : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createPlatformSpawn(deps) {
|
|
16
|
+
const {
|
|
17
|
+
fs,
|
|
18
|
+
path,
|
|
19
|
+
spawn,
|
|
20
|
+
execSync,
|
|
21
|
+
processPlatform = process.platform,
|
|
22
|
+
processExecPath = process.execPath,
|
|
23
|
+
claudeBin = '',
|
|
24
|
+
} = deps;
|
|
25
|
+
|
|
26
|
+
const nodeEntryCache = new Map();
|
|
27
|
+
|
|
28
|
+
function resolveNodeEntryForCmd(cmd) {
|
|
29
|
+
if (nodeEntryCache.has(cmd)) return nodeEntryCache.get(cmd);
|
|
30
|
+
let cmdPath = cmd;
|
|
31
|
+
const lowerCmd = String(cmd || '').toLowerCase();
|
|
32
|
+
if (lowerCmd === 'claude' || lowerCmd === 'codex') {
|
|
33
|
+
try {
|
|
34
|
+
const lines = execSync(`where ${cmd}`, { encoding: 'utf8', timeout: 3000 })
|
|
35
|
+
.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
36
|
+
cmdPath = lines.find((line) => line.toLowerCase().endsWith(`${lowerCmd}.cmd`)) || lines[0] || cmd;
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
}
|
|
39
|
+
const entry = resolveNodeEntry(fs, path, cmdPath);
|
|
40
|
+
nodeEntryCache.set(cmd, entry);
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function platformSpawn(cmd, args, options) {
|
|
45
|
+
if (processPlatform !== 'win32') return spawn(cmd, args, options);
|
|
46
|
+
|
|
47
|
+
const lowerCmd = String(cmd || '').toLowerCase();
|
|
48
|
+
const isCmdLike = lowerCmd.endsWith('.cmd') || lowerCmd.endsWith('.bat')
|
|
49
|
+
|| cmd === claudeBin || lowerCmd === 'claude' || lowerCmd === 'codex';
|
|
50
|
+
|
|
51
|
+
if (isCmdLike) {
|
|
52
|
+
const entry = resolveNodeEntryForCmd(cmd);
|
|
53
|
+
if (entry) {
|
|
54
|
+
return spawn(processExecPath, [entry, ...args], { ...options, windowsHide: true });
|
|
55
|
+
}
|
|
56
|
+
return spawn(cmd, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return spawn(cmd, args, { ...options, windowsHide: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
spawn: platformSpawn,
|
|
64
|
+
resolveNodeEntryForCmd,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function terminateChildProcess(child, signal = 'SIGTERM', opts = {}) {
|
|
69
|
+
if (!child) return false;
|
|
70
|
+
const useProcessGroup = opts.useProcessGroup !== false;
|
|
71
|
+
if (useProcessGroup) {
|
|
72
|
+
try {
|
|
73
|
+
process.kill(-child.pid, signal);
|
|
74
|
+
return true;
|
|
75
|
+
} catch { /* fall through */ }
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
child.kill(signal);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escalateKill(child, signal = 'SIGTERM', forceDelayMs = 5000, opts = {}) {
|
|
86
|
+
const signaled = terminateChildProcess(child, signal, opts);
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
terminateChildProcess(child, 'SIGKILL', opts);
|
|
89
|
+
}, forceDelayMs);
|
|
90
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
91
|
+
return { signaled, timer };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resetReusableChildListeners(child) {
|
|
95
|
+
if (!child) return child;
|
|
96
|
+
if (child.stdout && typeof child.stdout.removeAllListeners === 'function') {
|
|
97
|
+
child.stdout.removeAllListeners('data');
|
|
98
|
+
}
|
|
99
|
+
if (child.stderr && typeof child.stderr.removeAllListeners === 'function') {
|
|
100
|
+
child.stderr.removeAllListeners('data');
|
|
101
|
+
}
|
|
102
|
+
if (child.stdin && typeof child.stdin.removeAllListeners === 'function') {
|
|
103
|
+
child.stdin.removeAllListeners('error');
|
|
104
|
+
}
|
|
105
|
+
if (typeof child.removeAllListeners === 'function') {
|
|
106
|
+
child.removeAllListeners('close');
|
|
107
|
+
child.removeAllListeners('error');
|
|
108
|
+
}
|
|
109
|
+
return child;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function destroyChildStdin(child) {
|
|
113
|
+
if (!child || !child.stdin || typeof child.stdin.destroy !== 'function') return false;
|
|
114
|
+
try {
|
|
115
|
+
child.stdin.destroy();
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stopStreamingLifecycle(watchdog, milestoneTimer) {
|
|
123
|
+
if (watchdog && typeof watchdog.stop === 'function') watchdog.stop();
|
|
124
|
+
clearInterval(milestoneTimer);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function abortStreamingChildLifecycle(opts) {
|
|
128
|
+
const {
|
|
129
|
+
child,
|
|
130
|
+
watchdog,
|
|
131
|
+
milestoneTimer,
|
|
132
|
+
activeProcesses,
|
|
133
|
+
saveActivePids,
|
|
134
|
+
chatId,
|
|
135
|
+
reason = 'stdin',
|
|
136
|
+
} = opts;
|
|
137
|
+
|
|
138
|
+
clearInterval(milestoneTimer);
|
|
139
|
+
destroyChildStdin(child);
|
|
140
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
141
|
+
if (watchdog && typeof watchdog.abort === 'function') watchdog.abort(reason);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setActiveChildProcess(activeProcesses, saveActivePids, chatId, entry) {
|
|
145
|
+
if (!chatId || !activeProcesses || typeof activeProcesses.set !== 'function') return false;
|
|
146
|
+
activeProcesses.set(chatId, entry);
|
|
147
|
+
if (typeof saveActivePids === 'function') saveActivePids();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function clearActiveChildProcess(activeProcesses, saveActivePids, chatId) {
|
|
152
|
+
if (!chatId || !activeProcesses || typeof activeProcesses.delete !== 'function') return false;
|
|
153
|
+
activeProcesses.delete(chatId);
|
|
154
|
+
if (typeof saveActivePids === 'function') saveActivePids();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function acquireStreamingChild(opts) {
|
|
159
|
+
const {
|
|
160
|
+
warmChild = null,
|
|
161
|
+
spawn,
|
|
162
|
+
binary,
|
|
163
|
+
args,
|
|
164
|
+
cwd,
|
|
165
|
+
env,
|
|
166
|
+
useDetached = true,
|
|
167
|
+
} = opts;
|
|
168
|
+
|
|
169
|
+
if (warmChild) {
|
|
170
|
+
return {
|
|
171
|
+
child: resetReusableChildListeners(warmChild),
|
|
172
|
+
reused: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
child: spawn(binary, args, {
|
|
178
|
+
cwd,
|
|
179
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
|
+
detached: useDetached,
|
|
181
|
+
env,
|
|
182
|
+
}),
|
|
183
|
+
reused: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildStreamingResult(base, overrides = {}) {
|
|
188
|
+
return {
|
|
189
|
+
output: base.output,
|
|
190
|
+
error: base.error,
|
|
191
|
+
files: Array.isArray(base.files) ? base.files : [],
|
|
192
|
+
toolUsageLog: Array.isArray(base.toolUsageLog) ? base.toolUsageLog : [],
|
|
193
|
+
usage: base.usage || null,
|
|
194
|
+
sessionId: base.sessionId || '',
|
|
195
|
+
...overrides,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveStreamingClosePayload(opts) {
|
|
200
|
+
const {
|
|
201
|
+
code,
|
|
202
|
+
streamState: { finalResult, finalUsage, observedSessionId, writtenFiles, toolUsageLog } = {},
|
|
203
|
+
wasAborted = false,
|
|
204
|
+
abortReason = '',
|
|
205
|
+
stdinFailureError = null,
|
|
206
|
+
watchdog,
|
|
207
|
+
timeoutConfig: { startTime, idleTimeoutMs, toolTimeoutMs, hardCeilingMs, formatTimeoutWindowLabel } = {},
|
|
208
|
+
classifiedError,
|
|
209
|
+
stderr = '',
|
|
210
|
+
} = opts;
|
|
211
|
+
|
|
212
|
+
const base = {
|
|
213
|
+
output: finalResult || null,
|
|
214
|
+
error: null,
|
|
215
|
+
files: writtenFiles,
|
|
216
|
+
toolUsageLog,
|
|
217
|
+
usage: finalUsage,
|
|
218
|
+
sessionId: observedSessionId || '',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (wasAborted) {
|
|
222
|
+
const errorCode = (abortReason === 'daemon-restart' || abortReason === 'shutdown')
|
|
223
|
+
? 'INTERRUPTED_RESTART'
|
|
224
|
+
: abortReason === 'merge-pause'
|
|
225
|
+
? 'INTERRUPTED_MERGE_PAUSE'
|
|
226
|
+
: 'INTERRUPTED_USER';
|
|
227
|
+
return buildStreamingResult({
|
|
228
|
+
...base,
|
|
229
|
+
error: abortReason === 'merge-pause' ? 'Paused for merge' : 'Stopped by user',
|
|
230
|
+
}, { errorCode });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (stdinFailureError) {
|
|
234
|
+
return buildStreamingResult({
|
|
235
|
+
...base,
|
|
236
|
+
error: stdinFailureError,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (watchdog && typeof watchdog.isKilled === 'function' && watchdog.isKilled()) {
|
|
241
|
+
const elapsed = Math.round((Date.now() - startTime) / 60000);
|
|
242
|
+
const toolWindow = formatTimeoutWindowLabel(toolTimeoutMs, 'tool');
|
|
243
|
+
const idleWindow = formatTimeoutWindowLabel(idleTimeoutMs, 'idle');
|
|
244
|
+
const killedReason = typeof watchdog.getKilledReason === 'function' ? watchdog.getKilledReason() : 'idle';
|
|
245
|
+
const reason = killedReason === 'ceiling'
|
|
246
|
+
? `⏱ 已运行 ${elapsed} 分钟,达到上限(${Math.round(hardCeilingMs / 60000)} 分钟)`
|
|
247
|
+
: killedReason === 'tool'
|
|
248
|
+
? `⏱ 工具执行${toolWindow}超时,判定卡死(共运行 ${elapsed} 分钟)`
|
|
249
|
+
: `⏱ 已${idleWindow}无输出,判定卡死(共运行 ${elapsed} 分钟)`;
|
|
250
|
+
return buildStreamingResult({
|
|
251
|
+
...base,
|
|
252
|
+
error: reason,
|
|
253
|
+
}, { timedOut: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (code !== 0) {
|
|
257
|
+
return buildStreamingResult({
|
|
258
|
+
...base,
|
|
259
|
+
error: classifiedError && classifiedError.message
|
|
260
|
+
? classifiedError.message
|
|
261
|
+
: (stderr || `Exit code ${code}`),
|
|
262
|
+
}, { errorCode: classifiedError ? classifiedError.code : undefined });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return buildStreamingResult({
|
|
266
|
+
...base,
|
|
267
|
+
output: finalResult || '',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function accumulateStreamingStderr(state, chunk, opts = {}) {
|
|
272
|
+
const {
|
|
273
|
+
classifyError = null,
|
|
274
|
+
} = opts;
|
|
275
|
+
|
|
276
|
+
const nextState = {
|
|
277
|
+
stderr: `${state && state.stderr ? state.stderr : ''}${chunk}`,
|
|
278
|
+
classifiedError: state ? state.classifiedError || null : null,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
nextState.isApiError = /\b(400|is not supported|model.*not found|invalid.*model)\b/i.test(chunk);
|
|
282
|
+
if (!nextState.classifiedError && typeof classifyError === 'function') {
|
|
283
|
+
nextState.classifiedError = classifyError(chunk);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return nextState;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function splitStreamingStdoutChunk(buffer, chunk) {
|
|
290
|
+
const nextBuffer = `${buffer || ''}${chunk}`;
|
|
291
|
+
const lines = nextBuffer.split('\n');
|
|
292
|
+
return {
|
|
293
|
+
lines: lines.slice(0, -1),
|
|
294
|
+
buffer: lines[lines.length - 1] || '',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildStreamFlushPayload(state, opts = {}) {
|
|
299
|
+
const {
|
|
300
|
+
force = false,
|
|
301
|
+
now = Date.now(),
|
|
302
|
+
throttleMs = 1500,
|
|
303
|
+
} = opts;
|
|
304
|
+
const text = state && state.streamText ? String(state.streamText) : '';
|
|
305
|
+
const lastFlushAt = state && state.lastFlushAt ? state.lastFlushAt : 0;
|
|
306
|
+
|
|
307
|
+
if (!text.trim()) return { shouldFlush: false, lastFlushAt };
|
|
308
|
+
if (!force && now - lastFlushAt < throttleMs) {
|
|
309
|
+
return { shouldFlush: false, lastFlushAt };
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
shouldFlush: true,
|
|
313
|
+
lastFlushAt: now,
|
|
314
|
+
payload: `__STREAM_TEXT__${text}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildToolOverlayPayload(opts = {}) {
|
|
319
|
+
const {
|
|
320
|
+
toolName = 'Tool',
|
|
321
|
+
toolInput = {},
|
|
322
|
+
streamText = '',
|
|
323
|
+
lastStatusTime = 0,
|
|
324
|
+
now = Date.now(),
|
|
325
|
+
throttleMs = 3000,
|
|
326
|
+
toolEmoji = {},
|
|
327
|
+
pathModule = null,
|
|
328
|
+
} = opts;
|
|
329
|
+
|
|
330
|
+
if (now - lastStatusTime < throttleMs) {
|
|
331
|
+
return { shouldEmit: false, lastStatusTime };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const emoji = toolEmoji[toolName] || toolEmoji.default || '';
|
|
335
|
+
let displayName = toolName;
|
|
336
|
+
let displayEmoji = emoji;
|
|
337
|
+
let context = '';
|
|
338
|
+
|
|
339
|
+
if (toolName === 'Skill' && toolInput.skill) {
|
|
340
|
+
context = toolInput.skill;
|
|
341
|
+
} else if ((toolName === 'Task' || toolName === 'Agent') && toolInput.description) {
|
|
342
|
+
const agentType = toolInput.subagent_type ? `[${toolInput.subagent_type}] ` : '';
|
|
343
|
+
context = (agentType + String(toolInput.description)).slice(0, 40);
|
|
344
|
+
} else if (toolName.startsWith('mcp__')) {
|
|
345
|
+
const parts = toolName.split('__');
|
|
346
|
+
const server = parts[1] || 'unknown';
|
|
347
|
+
const action = parts.slice(2).join('_') || '';
|
|
348
|
+
if (server === 'playwright') {
|
|
349
|
+
displayEmoji = '🌐';
|
|
350
|
+
displayName = 'Browser';
|
|
351
|
+
context = action.replace(/_/g, ' ');
|
|
352
|
+
} else {
|
|
353
|
+
displayEmoji = '🔗';
|
|
354
|
+
displayName = `MCP:${server}`;
|
|
355
|
+
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
356
|
+
}
|
|
357
|
+
} else if (toolInput.file_path && pathModule) {
|
|
358
|
+
const basename = pathModule.basename(String(toolInput.file_path));
|
|
359
|
+
const dotIdx = basename.lastIndexOf('.');
|
|
360
|
+
context = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
361
|
+
} else if (toolInput.command) {
|
|
362
|
+
context = String(toolInput.command).slice(0, 30);
|
|
363
|
+
if (String(toolInput.command).length > 30) context += '...';
|
|
364
|
+
} else if (toolInput.pattern) {
|
|
365
|
+
context = String(toolInput.pattern).slice(0, 20);
|
|
366
|
+
} else if (toolInput.query) {
|
|
367
|
+
context = String(toolInput.query).slice(0, 25);
|
|
368
|
+
} else if (toolInput.url) {
|
|
369
|
+
try { context = new URL(toolInput.url).hostname; } catch { context = 'web'; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const status = context
|
|
373
|
+
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
374
|
+
: `${displayEmoji} ${displayName}...`;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
shouldEmit: true,
|
|
378
|
+
lastStatusTime: now,
|
|
379
|
+
payload: streamText ? `__TOOL_OVERLAY__${streamText}\n\n> ${status}` : status,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function recordToolUsage(state, opts = {}) {
|
|
384
|
+
const {
|
|
385
|
+
toolName = 'Tool',
|
|
386
|
+
toolInput = {},
|
|
387
|
+
pathModule = null,
|
|
388
|
+
maxEntries = 50,
|
|
389
|
+
} = opts;
|
|
390
|
+
|
|
391
|
+
const toolUsageLog = Array.isArray(state && state.toolUsageLog) ? [...state.toolUsageLog] : [];
|
|
392
|
+
const writtenFiles = Array.isArray(state && state.writtenFiles) ? [...state.writtenFiles] : [];
|
|
393
|
+
|
|
394
|
+
const toolEntry = { tool: toolName };
|
|
395
|
+
if (toolName === 'Skill' && toolInput.skill) toolEntry.skill = toolInput.skill;
|
|
396
|
+
else if (toolInput.command) toolEntry.context = String(toolInput.command).slice(0, 50);
|
|
397
|
+
else if (toolInput.file_path && pathModule) toolEntry.context = pathModule.basename(String(toolInput.file_path));
|
|
398
|
+
|
|
399
|
+
if (toolUsageLog.length < maxEntries) toolUsageLog.push(toolEntry);
|
|
400
|
+
|
|
401
|
+
if (toolName === 'Write' && toolInput.file_path) {
|
|
402
|
+
const filePath = String(toolInput.file_path);
|
|
403
|
+
if (!writtenFiles.includes(filePath)) writtenFiles.push(filePath);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { toolUsageLog, writtenFiles };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildMilestoneOverlayPayload(opts = {}) {
|
|
410
|
+
const {
|
|
411
|
+
elapsedMin,
|
|
412
|
+
toolCallCount = 0,
|
|
413
|
+
writtenFiles = [],
|
|
414
|
+
toolUsageLog = [],
|
|
415
|
+
streamText = '',
|
|
416
|
+
} = opts;
|
|
417
|
+
|
|
418
|
+
const parts = [`⏳ 已运行 ${elapsedMin} 分钟`];
|
|
419
|
+
if (toolCallCount > 0) parts.push(`调用 ${toolCallCount} 次工具`);
|
|
420
|
+
if (writtenFiles.length > 0) parts.push(`修改 ${writtenFiles.length} 个文件`);
|
|
421
|
+
|
|
422
|
+
const recentTool = toolUsageLog.length > 0 ? toolUsageLog[toolUsageLog.length - 1] : null;
|
|
423
|
+
if (recentTool) {
|
|
424
|
+
const ctx = recentTool.context || recentTool.skill || '';
|
|
425
|
+
parts.push(`最近: ${recentTool.tool}${ctx ? ' ' + ctx : ''}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const milestoneMsg = parts.join(' | ');
|
|
429
|
+
return streamText ? `__TOOL_OVERLAY__${streamText}\n\n> ${milestoneMsg}` : milestoneMsg;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function finalizePersistentStreamingTurn(opts = {}) {
|
|
433
|
+
const {
|
|
434
|
+
watchdog,
|
|
435
|
+
milestoneTimer,
|
|
436
|
+
activeProcesses,
|
|
437
|
+
saveActivePids,
|
|
438
|
+
chatId,
|
|
439
|
+
warmPool = null,
|
|
440
|
+
warmSessionKey = '',
|
|
441
|
+
child = null,
|
|
442
|
+
observedSessionId = '',
|
|
443
|
+
cwd = '',
|
|
444
|
+
output = '',
|
|
445
|
+
files = [],
|
|
446
|
+
toolUsageLog = [],
|
|
447
|
+
usage = null,
|
|
448
|
+
} = opts;
|
|
449
|
+
|
|
450
|
+
if (watchdog && typeof watchdog.stop === 'function') watchdog.stop();
|
|
451
|
+
clearInterval(milestoneTimer);
|
|
452
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
453
|
+
if (warmPool && warmSessionKey && child && !child.killed && child.exitCode === null) {
|
|
454
|
+
warmPool.storeWarm(warmSessionKey, child, { sessionId: observedSessionId, cwd });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return buildStreamingResult({
|
|
458
|
+
output,
|
|
459
|
+
error: null,
|
|
460
|
+
files,
|
|
461
|
+
toolUsageLog,
|
|
462
|
+
usage,
|
|
463
|
+
sessionId: observedSessionId || '',
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function writeStreamingChildInput(opts = {}) {
|
|
468
|
+
const {
|
|
469
|
+
child,
|
|
470
|
+
input = '',
|
|
471
|
+
isPersistent = false,
|
|
472
|
+
warmPool = null,
|
|
473
|
+
observedSessionId = '',
|
|
474
|
+
} = opts;
|
|
475
|
+
|
|
476
|
+
if (isPersistent && warmPool) {
|
|
477
|
+
child.stdin.write(warmPool.buildStreamMessage(input, observedSessionId || ''));
|
|
478
|
+
return { mode: 'persistent' };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
child.stdin.write(input);
|
|
482
|
+
child.stdin.end();
|
|
483
|
+
return { mode: 'oneshot' };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function parseStreamingEvents(parseStreamEvent, line) {
|
|
487
|
+
try {
|
|
488
|
+
return parseStreamEvent(line) || [];
|
|
489
|
+
} catch {
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function reduceStreamingWaitState(waitingForTool, eventType) {
|
|
495
|
+
if (eventType === 'tool_use') {
|
|
496
|
+
return { waitingForTool: true, shouldUpdateWatchdog: !waitingForTool, watchdogWaiting: true };
|
|
497
|
+
}
|
|
498
|
+
if ((eventType === 'text' || eventType === 'done' || eventType === 'tool_result') && waitingForTool) {
|
|
499
|
+
return { waitingForTool: false, shouldUpdateWatchdog: true, watchdogWaiting: false };
|
|
500
|
+
}
|
|
501
|
+
return { waitingForTool, shouldUpdateWatchdog: false, watchdogWaiting: waitingForTool };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function applyStreamingTextResult(state, opts = {}) {
|
|
505
|
+
const {
|
|
506
|
+
eventType,
|
|
507
|
+
text = '',
|
|
508
|
+
doneResult = '',
|
|
509
|
+
} = opts;
|
|
510
|
+
let finalResult = state && typeof state.finalResult === 'string' ? state.finalResult : '';
|
|
511
|
+
let streamText = state && typeof state.streamText === 'string' ? state.streamText : finalResult;
|
|
512
|
+
|
|
513
|
+
if (eventType === 'text' && text) {
|
|
514
|
+
finalResult += (finalResult ? '\n\n' : '') + String(text);
|
|
515
|
+
streamText = finalResult;
|
|
516
|
+
}
|
|
517
|
+
if (eventType === 'done' && !finalResult && doneResult) {
|
|
518
|
+
finalResult = String(doneResult);
|
|
519
|
+
streamText = finalResult;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return { finalResult, streamText };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function applyStreamingMetadata(state, event) {
|
|
526
|
+
return {
|
|
527
|
+
observedSessionId: event && event.type === 'session' && event.sessionId
|
|
528
|
+
? String(event.sessionId)
|
|
529
|
+
: (state && state.observedSessionId ? state.observedSessionId : ''),
|
|
530
|
+
classifiedError: event && event.type === 'error'
|
|
531
|
+
? event
|
|
532
|
+
: (state ? state.classifiedError || null : null),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function applyStreamingToolState(state, event, opts = {}) {
|
|
537
|
+
const {
|
|
538
|
+
pathModule,
|
|
539
|
+
maxEntries = 50,
|
|
540
|
+
} = opts;
|
|
541
|
+
const eventType = event && event.type ? event.type : '';
|
|
542
|
+
const waitState = reduceStreamingWaitState(state && state.waitingForTool, eventType);
|
|
543
|
+
const nextState = {
|
|
544
|
+
toolCallCount: state && Number.isFinite(state.toolCallCount) ? state.toolCallCount : 0,
|
|
545
|
+
waitingForTool: waitState.waitingForTool,
|
|
546
|
+
shouldUpdateWatchdog: waitState.shouldUpdateWatchdog,
|
|
547
|
+
watchdogWaiting: waitState.watchdogWaiting,
|
|
548
|
+
toolUsageLog: Array.isArray(state && state.toolUsageLog) ? state.toolUsageLog.slice() : [],
|
|
549
|
+
writtenFiles: Array.isArray(state && state.writtenFiles) ? state.writtenFiles.slice() : [],
|
|
550
|
+
toolName: event && event.toolName ? event.toolName : 'Tool',
|
|
551
|
+
toolInput: event && event.toolInput ? event.toolInput : {},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (eventType !== 'tool_use') return nextState;
|
|
555
|
+
|
|
556
|
+
nextState.toolCallCount += 1;
|
|
557
|
+
const toolState = recordToolUsage(
|
|
558
|
+
{ toolUsageLog: nextState.toolUsageLog, writtenFiles: nextState.writtenFiles },
|
|
559
|
+
{
|
|
560
|
+
toolName: nextState.toolName,
|
|
561
|
+
toolInput: nextState.toolInput,
|
|
562
|
+
pathModule,
|
|
563
|
+
maxEntries,
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
nextState.toolUsageLog = toolState.toolUsageLog;
|
|
567
|
+
nextState.writtenFiles = toolState.writtenFiles;
|
|
568
|
+
return nextState;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function applyStreamingContentState(state, event) {
|
|
572
|
+
const eventType = event && event.type ? event.type : '';
|
|
573
|
+
const waitState = reduceStreamingWaitState(state && state.waitingForTool, eventType);
|
|
574
|
+
const textState = applyStreamingTextResult(
|
|
575
|
+
{
|
|
576
|
+
finalResult: state && state.finalResult,
|
|
577
|
+
streamText: state && state.streamText,
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
eventType,
|
|
581
|
+
text: event && event.text,
|
|
582
|
+
doneResult: event && event.result,
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
return {
|
|
586
|
+
finalResult: textState.finalResult,
|
|
587
|
+
streamText: textState.streamText,
|
|
588
|
+
waitingForTool: waitState.waitingForTool,
|
|
589
|
+
shouldUpdateWatchdog: waitState.shouldUpdateWatchdog,
|
|
590
|
+
watchdogWaiting: waitState.watchdogWaiting,
|
|
591
|
+
finalUsage: eventType === 'done' ? (event.usage || null) : (state ? state.finalUsage || null : null),
|
|
592
|
+
shouldFlush: eventType === 'text' || eventType === 'done',
|
|
593
|
+
flushForce: eventType === 'done',
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function createStreamingWatchdog(opts) {
|
|
598
|
+
const {
|
|
599
|
+
child,
|
|
600
|
+
killSignal = 'SIGTERM',
|
|
601
|
+
useProcessGroup = true,
|
|
602
|
+
idleTimeoutMs,
|
|
603
|
+
toolTimeoutMs,
|
|
604
|
+
ceilingTimeoutMs = null,
|
|
605
|
+
forceKillDelayMs = 5000,
|
|
606
|
+
onKill = null,
|
|
607
|
+
} = opts;
|
|
608
|
+
|
|
609
|
+
let waitingForTool = false;
|
|
610
|
+
let killed = false;
|
|
611
|
+
let killedReason = null;
|
|
612
|
+
let sigkillTimer = null;
|
|
613
|
+
|
|
614
|
+
function kill(reason) {
|
|
615
|
+
if (killed) return;
|
|
616
|
+
killed = true;
|
|
617
|
+
killedReason = reason;
|
|
618
|
+
if (typeof onKill === 'function') onKill(reason);
|
|
619
|
+
({ timer: sigkillTimer } = escalateKill(child, killSignal, forceKillDelayMs, { useProcessGroup }));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let idleTimer = setTimeout(() => kill('idle'), idleTimeoutMs);
|
|
623
|
+
const ceilingTimer = ceilingTimeoutMs
|
|
624
|
+
? setTimeout(() => kill('ceiling'), ceilingTimeoutMs)
|
|
625
|
+
: null;
|
|
626
|
+
|
|
627
|
+
function resetIdle() {
|
|
628
|
+
clearTimeout(idleTimer);
|
|
629
|
+
const timeout = waitingForTool ? toolTimeoutMs : idleTimeoutMs;
|
|
630
|
+
idleTimer = setTimeout(() => kill(waitingForTool ? 'tool' : 'idle'), timeout);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function setWaitingForTool(next) {
|
|
634
|
+
waitingForTool = !!next;
|
|
635
|
+
resetIdle();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function abort(reason = 'stdin') {
|
|
639
|
+
clearTimeout(idleTimer);
|
|
640
|
+
clearTimeout(ceilingTimer);
|
|
641
|
+
kill(reason);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function stop() {
|
|
645
|
+
clearTimeout(idleTimer);
|
|
646
|
+
clearTimeout(ceilingTimer);
|
|
647
|
+
clearTimeout(sigkillTimer);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
resetIdle,
|
|
652
|
+
setWaitingForTool,
|
|
653
|
+
abort,
|
|
654
|
+
stop,
|
|
655
|
+
isKilled() { return killed; },
|
|
656
|
+
getKilledReason() { return killedReason; },
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function runAsyncCommand(opts) {
|
|
661
|
+
const {
|
|
662
|
+
spawn,
|
|
663
|
+
cmd,
|
|
664
|
+
args,
|
|
665
|
+
cwd,
|
|
666
|
+
env,
|
|
667
|
+
input = '',
|
|
668
|
+
timeoutMs = 300000,
|
|
669
|
+
killSignal = 'SIGTERM',
|
|
670
|
+
useProcessGroup = false,
|
|
671
|
+
forceKillDelayMs = 5000,
|
|
672
|
+
formatSpawnError = (err) => err && err.message ? err.message : String(err || 'Unknown spawn error'),
|
|
673
|
+
} = opts;
|
|
674
|
+
|
|
675
|
+
return new Promise((resolve) => {
|
|
676
|
+
let settled = false;
|
|
677
|
+
function finalize(payload) {
|
|
678
|
+
if (settled) return;
|
|
679
|
+
settled = true;
|
|
680
|
+
resolve(payload);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const child = spawn(cmd, args, {
|
|
684
|
+
cwd,
|
|
685
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
686
|
+
env,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
let stdout = '';
|
|
690
|
+
let stderr = '';
|
|
691
|
+
let timedOut = false;
|
|
692
|
+
let sigkillTimer = null;
|
|
693
|
+
let stdinFailureError = null;
|
|
694
|
+
|
|
695
|
+
function abortForStdinFailure(err) {
|
|
696
|
+
if (stdinFailureError) return;
|
|
697
|
+
stdinFailureError = formatSpawnError(err);
|
|
698
|
+
clearTimeout(timer);
|
|
699
|
+
destroyChildStdin(child);
|
|
700
|
+
if (!sigkillTimer) {
|
|
701
|
+
({ timer: sigkillTimer } = escalateKill(child, killSignal, forceKillDelayMs, { useProcessGroup }));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const timer = setTimeout(() => {
|
|
706
|
+
timedOut = true;
|
|
707
|
+
({ timer: sigkillTimer } = escalateKill(child, killSignal, forceKillDelayMs, { useProcessGroup }));
|
|
708
|
+
}, timeoutMs);
|
|
709
|
+
|
|
710
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
711
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
712
|
+
if (child.stdin && typeof child.stdin.on === 'function') {
|
|
713
|
+
child.stdin.on('error', (err) => { abortForStdinFailure(err); });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
child.on('close', (code) => {
|
|
717
|
+
clearTimeout(timer);
|
|
718
|
+
clearTimeout(sigkillTimer);
|
|
719
|
+
if (stdinFailureError) {
|
|
720
|
+
finalize({ output: null, error: stdinFailureError });
|
|
721
|
+
} else if (timedOut) {
|
|
722
|
+
finalize({ output: null, error: `Timeout: engine took too long (${Math.round(timeoutMs / 1000)}s)` });
|
|
723
|
+
} else if (code !== 0) {
|
|
724
|
+
finalize({ output: null, error: stderr || `Exit code ${code}` });
|
|
725
|
+
} else {
|
|
726
|
+
finalize({ output: stdout.trim(), error: null });
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
child.on('error', (err) => {
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
clearTimeout(sigkillTimer);
|
|
733
|
+
finalize({ output: null, error: formatSpawnError(err) });
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
child.stdin.write(input);
|
|
738
|
+
child.stdin.end();
|
|
739
|
+
} catch (err) {
|
|
740
|
+
abortForStdinFailure(err);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Public API — consumed by daemon-claude-engine.js
|
|
746
|
+
module.exports = {
|
|
747
|
+
createPlatformSpawn,
|
|
748
|
+
terminateChildProcess,
|
|
749
|
+
stopStreamingLifecycle,
|
|
750
|
+
abortStreamingChildLifecycle,
|
|
751
|
+
setActiveChildProcess,
|
|
752
|
+
clearActiveChildProcess,
|
|
753
|
+
acquireStreamingChild,
|
|
754
|
+
buildStreamingResult,
|
|
755
|
+
resolveStreamingClosePayload,
|
|
756
|
+
accumulateStreamingStderr,
|
|
757
|
+
splitStreamingStdoutChunk,
|
|
758
|
+
buildStreamFlushPayload,
|
|
759
|
+
buildToolOverlayPayload,
|
|
760
|
+
buildMilestoneOverlayPayload,
|
|
761
|
+
finalizePersistentStreamingTurn,
|
|
762
|
+
writeStreamingChildInput,
|
|
763
|
+
parseStreamingEvents,
|
|
764
|
+
applyStreamingMetadata,
|
|
765
|
+
applyStreamingToolState,
|
|
766
|
+
applyStreamingContentState,
|
|
767
|
+
createStreamingWatchdog,
|
|
768
|
+
runAsyncCommand,
|
|
769
|
+
|
|
770
|
+
// Internal helpers — exported for unit test coverage only
|
|
771
|
+
_internal: {
|
|
772
|
+
resolveNodeEntry,
|
|
773
|
+
escalateKill,
|
|
774
|
+
resetReusableChildListeners,
|
|
775
|
+
destroyChildStdin,
|
|
776
|
+
recordToolUsage,
|
|
777
|
+
reduceStreamingWaitState,
|
|
778
|
+
applyStreamingTextResult,
|
|
779
|
+
},
|
|
780
|
+
};
|