metame-cli 1.5.18 → 1.5.20
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 +79 -35
- package/scripts/daemon-claude-engine.js +371 -425
- package/scripts/daemon-command-router.js +80 -6
- package/scripts/daemon-engine-runtime.js +26 -4
- package/scripts/daemon-message-pipeline.js +2 -2
- package/scripts/daemon-reactive-lifecycle.js +134 -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 +37 -176
- 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
|
@@ -10,7 +10,9 @@ const {
|
|
|
10
10
|
_private: { resolveCodexPermissionProfile },
|
|
11
11
|
} = require('./daemon-engine-runtime');
|
|
12
12
|
const { buildIntentHintBlock } = require('./intent-registry');
|
|
13
|
+
const { rawChatId } = require('./core/thread-chat-id');
|
|
13
14
|
const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
|
|
15
|
+
const { createPlatformSpawn, terminateChildProcess, stopStreamingLifecycle, abortStreamingChildLifecycle, setActiveChildProcess, clearActiveChildProcess, acquireStreamingChild, buildStreamingResult, resolveStreamingClosePayload, accumulateStreamingStderr, splitStreamingStdoutChunk, buildStreamFlushPayload, buildToolOverlayPayload, buildMilestoneOverlayPayload, finalizePersistentStreamingTurn, writeStreamingChildInput, parseStreamingEvents, applyStreamingMetadata, applyStreamingToolState, applyStreamingContentState, createStreamingWatchdog, runAsyncCommand } = require('./core/handoff');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Antigravity Raw Session Logging — Lossless Diary (L0)
|
|
@@ -33,6 +35,22 @@ function logRawSessionDiary(fs, path, HOME, { chatId, prompt, output, error, pro
|
|
|
33
35
|
} catch (e) { console.warn(`[MetaMe] Raw session logging failed: ${e.message}`); }
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
function resolveStreamingTimeouts(engineTimeouts = {}) {
|
|
39
|
+
return {
|
|
40
|
+
idleMs: engineTimeouts.idleMs ?? (5 * 60 * 1000),
|
|
41
|
+
toolMs: engineTimeouts.toolMs ?? (25 * 60 * 1000),
|
|
42
|
+
ceilingMs: engineTimeouts.ceilingMs ?? (60 * 60 * 1000),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTimeoutWindowLabel(timeoutMs, kind = 'idle') {
|
|
47
|
+
const mins = Math.round(Number(timeoutMs || 0) / 60000);
|
|
48
|
+
if (mins <= 0) {
|
|
49
|
+
return kind === 'tool' ? '立即' : '立即';
|
|
50
|
+
}
|
|
51
|
+
return `${Math.max(1, mins)} 分钟`;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
function createClaudeEngine(deps) {
|
|
37
55
|
const {
|
|
38
56
|
fs,
|
|
@@ -64,6 +82,7 @@ function createClaudeEngine(deps) {
|
|
|
64
82
|
getSessionName,
|
|
65
83
|
writeSessionName,
|
|
66
84
|
markSessionStarted,
|
|
85
|
+
stripThinkingSignatures,
|
|
67
86
|
isEngineSessionValid,
|
|
68
87
|
getCodexSessionSandboxProfile,
|
|
69
88
|
getCodexSessionPermissionMode,
|
|
@@ -136,62 +155,15 @@ function createClaudeEngine(deps) {
|
|
|
136
155
|
const getEngineRuntime = typeof injectedGetEngineRuntime === 'function'
|
|
137
156
|
? injectedGetEngineRuntime
|
|
138
157
|
: createEngineRuntimeFactory({ fs, path, HOME, CLAUDE_BIN, getActiveProviderEnv });
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (m) {
|
|
149
|
-
// Substitute %dp0% (batch var for the cmd file's own directory)
|
|
150
|
-
const entry = m[1].replace(/%dp0%/gi, path.dirname(cmdPath) + path.sep);
|
|
151
|
-
if (fs.existsSync(entry)) return entry;
|
|
152
|
-
}
|
|
153
|
-
} catch { /* ignore */ }
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Cache resolved entries so we only read .cmd files once
|
|
158
|
-
const _nodeEntryCache = new Map();
|
|
159
|
-
function resolveNodeEntryForCmd(cmd) {
|
|
160
|
-
if (_nodeEntryCache.has(cmd)) return _nodeEntryCache.get(cmd);
|
|
161
|
-
let cmdPath = cmd;
|
|
162
|
-
const lowerCmd = String(cmd || '').toLowerCase();
|
|
163
|
-
// If bare name (not a file path), find the .cmd via where
|
|
164
|
-
if (lowerCmd === 'claude' || lowerCmd === 'codex') {
|
|
165
|
-
try {
|
|
166
|
-
const { execSync: _es } = require('child_process');
|
|
167
|
-
const lines = _es(`where ${cmd}`, { encoding: 'utf8', timeout: 3000 })
|
|
168
|
-
.split('\n').map(l => l.trim()).filter(Boolean);
|
|
169
|
-
cmdPath = lines.find(l => l.toLowerCase().endsWith(`${lowerCmd}.cmd`)) || lines[0] || cmd;
|
|
170
|
-
} catch { /* ignore */ }
|
|
171
|
-
}
|
|
172
|
-
const entry = resolveNodeEntry(cmdPath);
|
|
173
|
-
_nodeEntryCache.set(cmd, entry);
|
|
174
|
-
return entry;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function spawn(cmd, args, options) {
|
|
178
|
-
if (process.platform !== 'win32') return _spawn(cmd, args, options);
|
|
179
|
-
|
|
180
|
-
const lowerCmd = String(cmd || '').toLowerCase();
|
|
181
|
-
const isCmdLike = lowerCmd.endsWith('.cmd') || lowerCmd.endsWith('.bat')
|
|
182
|
-
|| cmd === CLAUDE_BIN || lowerCmd === 'claude' || lowerCmd === 'codex';
|
|
183
|
-
|
|
184
|
-
if (isCmdLike) {
|
|
185
|
-
const entry = resolveNodeEntryForCmd(cmd);
|
|
186
|
-
if (entry) {
|
|
187
|
-
// Run node directly — no cmd.exe, no flash
|
|
188
|
-
return _spawn(process.execPath, [entry, ...args], { ...options, windowsHide: true });
|
|
189
|
-
}
|
|
190
|
-
// Fallback: shell with windowsHide
|
|
191
|
-
return _spawn(cmd, args, { ...options, shell: process.env.COMSPEC || true, windowsHide: true });
|
|
192
|
-
}
|
|
193
|
-
return _spawn(cmd, args, { ...options, windowsHide: true });
|
|
194
|
-
}
|
|
158
|
+
const { spawn } = createPlatformSpawn({
|
|
159
|
+
fs,
|
|
160
|
+
path,
|
|
161
|
+
spawn: _spawn,
|
|
162
|
+
execSync: require('child_process').execSync,
|
|
163
|
+
processPlatform: process.platform,
|
|
164
|
+
processExecPath: process.execPath,
|
|
165
|
+
claudeBin: CLAUDE_BIN,
|
|
166
|
+
});
|
|
195
167
|
|
|
196
168
|
// Per-chatId patch queues: Agent A's writes never block Agent B.
|
|
197
169
|
const _patchQueues = new Map(); // chatId -> Promise
|
|
@@ -278,10 +250,14 @@ function createClaudeEngine(deps) {
|
|
|
278
250
|
}
|
|
279
251
|
|
|
280
252
|
function getSessionChatId(chatId, boundProjectKey) {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
253
|
+
const chatIdStr = String(chatId || '');
|
|
254
|
+
if (chatIdStr.startsWith('_agent_') || chatIdStr.startsWith('_scope_')) return chatIdStr;
|
|
255
|
+
// Topic threads get their own session even within a bound project —
|
|
256
|
+
// "thread:oc_xxx:om_yyy" must NOT collapse to "_bound_jarvis"
|
|
257
|
+
const { isThreadChatId } = require('./core/thread-chat-id');
|
|
258
|
+
if (isThreadChatId(chatIdStr)) return chatIdStr;
|
|
283
259
|
if (boundProjectKey) return `_bound_${boundProjectKey}`;
|
|
284
|
-
return
|
|
260
|
+
return chatIdStr || chatId;
|
|
285
261
|
}
|
|
286
262
|
|
|
287
263
|
function normalizeCodexSandboxMode(value, fallback = null) {
|
|
@@ -398,10 +374,10 @@ function createClaudeEngine(deps) {
|
|
|
398
374
|
function getActualCodexPermissionProfile(session) {
|
|
399
375
|
if (!session || !session.id) return null;
|
|
400
376
|
if (typeof getCodexSessionSandboxProfile === 'function') {
|
|
401
|
-
return getCodexSessionSandboxProfile(session.id);
|
|
377
|
+
return getCodexSessionSandboxProfile(session.id, session.cwd || '');
|
|
402
378
|
}
|
|
403
379
|
if (typeof getCodexSessionPermissionMode === 'function') {
|
|
404
|
-
const permissionMode = getCodexSessionPermissionMode(session.id);
|
|
380
|
+
const permissionMode = getCodexSessionPermissionMode(session.id, session.cwd || '');
|
|
405
381
|
return permissionMode ? { sandboxMode: permissionMode, approvalPolicy: null, permissionMode } : null;
|
|
406
382
|
}
|
|
407
383
|
return null;
|
|
@@ -434,10 +410,6 @@ function createClaudeEngine(deps) {
|
|
|
434
410
|
const entry = JSON.parse(line);
|
|
435
411
|
const sessionModel = entry && entry.message && entry.message.model;
|
|
436
412
|
if (!sessionModel || sessionModel === '<synthetic>') continue;
|
|
437
|
-
// Custom Claude-compatible providers may record backend-native model ids
|
|
438
|
-
// (for example MiniMax) in the JSONL. Those sessions are still resumable;
|
|
439
|
-
// we only skip model pinning when the family cannot be mapped back to
|
|
440
|
-
// Claude's alias set.
|
|
441
413
|
// If the configured model is a short alias (sonnet/opus/haiku) and the JSONL model
|
|
442
414
|
// belongs to the same family, do NOT pin — let the alias resolve to the latest version.
|
|
443
415
|
// Only pin when the families genuinely differ (e.g. session was opus, config says sonnet).
|
|
@@ -466,7 +438,7 @@ function createClaudeEngine(deps) {
|
|
|
466
438
|
|
|
467
439
|
function isClaudeThinkingSignatureError(errMsg) {
|
|
468
440
|
const msg = String(errMsg || '');
|
|
469
|
-
return msg.includes('Invalid signature')
|
|
441
|
+
return msg.includes('Invalid signature') && msg.includes('thinking block');
|
|
470
442
|
}
|
|
471
443
|
|
|
472
444
|
function formatClaudeResumeFallbackUserMessage(retryError) {
|
|
@@ -508,6 +480,24 @@ function createClaudeEngine(deps) {
|
|
|
508
480
|
retryPromptPrefix: '[Note: the previous Codex execution was interrupted by a daemon restart or user stop signal. Continue the same conversation if possible. User message follows:]',
|
|
509
481
|
};
|
|
510
482
|
}
|
|
483
|
+
const transportInterrupted = (
|
|
484
|
+
lowered.includes('stream disconnected')
|
|
485
|
+
|| lowered.includes('connection reset')
|
|
486
|
+
|| lowered.includes('connection aborted')
|
|
487
|
+
|| lowered.includes('broken pipe')
|
|
488
|
+
|| lowered.includes('timed out')
|
|
489
|
+
|| lowered.includes('timeout')
|
|
490
|
+
|| lowered.includes('temporarily unavailable')
|
|
491
|
+
|| lowered.includes('error sending request')
|
|
492
|
+
|| lowered.includes('http2')
|
|
493
|
+
);
|
|
494
|
+
if (transportInterrupted) {
|
|
495
|
+
return {
|
|
496
|
+
kind: 'transport',
|
|
497
|
+
userMessage: '⚠️ Codex 续接时网络/传输中断。系统正在优先重试同一条会话,不按 session 过期处理。',
|
|
498
|
+
retryPromptPrefix: '[Note: the previous Codex resume attempt was interrupted by a transient transport error. Continue the same conversation if possible. User message follows:]',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
511
501
|
return {
|
|
512
502
|
kind: 'expired',
|
|
513
503
|
userMessage: '⚠️ Codex session 已过期,上下文可能丢失。正在以全新 session 重试,请在回复后补充必要背景。',
|
|
@@ -711,54 +701,25 @@ function createClaudeEngine(deps) {
|
|
|
711
701
|
* Returns { output, error } after process exits.
|
|
712
702
|
*/
|
|
713
703
|
function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000, metameProject = '') {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
killed = true;
|
|
734
|
-
try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
|
|
735
|
-
setTimeout(() => {
|
|
736
|
-
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { } }
|
|
737
|
-
}, 5000);
|
|
738
|
-
}, timeoutMs);
|
|
739
|
-
|
|
740
|
-
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
741
|
-
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
742
|
-
|
|
743
|
-
child.on('close', (code) => {
|
|
744
|
-
clearTimeout(timer);
|
|
745
|
-
if (killed) {
|
|
746
|
-
resolve({ output: null, error: 'Timeout: Claude took too long' });
|
|
747
|
-
} else if (code !== 0) {
|
|
748
|
-
resolve({ output: null, error: stderr || `Exit code ${code}` });
|
|
749
|
-
} else {
|
|
750
|
-
resolve({ output: stdout.trim(), error: null });
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
child.on('error', (err) => {
|
|
755
|
-
clearTimeout(timer);
|
|
756
|
-
resolve({ output: null, error: formatEngineSpawnError(err, { name: getDefaultEngine() }) });
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
// Write input and close stdin
|
|
760
|
-
child.stdin.write(input);
|
|
761
|
-
child.stdin.end();
|
|
704
|
+
const env = {
|
|
705
|
+
...process.env,
|
|
706
|
+
...getActiveProviderEnv(),
|
|
707
|
+
METAME_INTERNAL_PROMPT: '1',
|
|
708
|
+
METAME_PROJECT: metameProject || '',
|
|
709
|
+
};
|
|
710
|
+
delete env.CLAUDECODE;
|
|
711
|
+
return runAsyncCommand({
|
|
712
|
+
spawn,
|
|
713
|
+
cmd: CLAUDE_BIN,
|
|
714
|
+
args,
|
|
715
|
+
cwd,
|
|
716
|
+
env,
|
|
717
|
+
input,
|
|
718
|
+
timeoutMs,
|
|
719
|
+
killSignal: 'SIGTERM',
|
|
720
|
+
useProcessGroup: false,
|
|
721
|
+
forceKillDelayMs: 5000,
|
|
722
|
+
formatSpawnError: (err) => formatEngineSpawnError(err, { name: 'claude' }),
|
|
762
723
|
});
|
|
763
724
|
}
|
|
764
725
|
|
|
@@ -814,27 +775,20 @@ function createClaudeEngine(deps) {
|
|
|
814
775
|
: args;
|
|
815
776
|
const _spawnAt = Date.now();
|
|
816
777
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
cwd,
|
|
829
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
830
|
-
detached: process.platform !== 'win32',
|
|
831
|
-
env: rt.buildEnv({ metameProject, metameSenderId }),
|
|
832
|
-
});
|
|
833
|
-
log('INFO', `[TIMING:${chatId}] spawned ${rt.name} pid=${child.pid}`);
|
|
834
|
-
}
|
|
778
|
+
const { child, reused } = acquireStreamingChild({
|
|
779
|
+
warmChild,
|
|
780
|
+
spawn,
|
|
781
|
+
binary: rt.binary,
|
|
782
|
+
args: streamArgs,
|
|
783
|
+
cwd,
|
|
784
|
+
env: rt.buildEnv({ metameProject, metameSenderId, cwd }),
|
|
785
|
+
useDetached: process.platform !== 'win32',
|
|
786
|
+
});
|
|
787
|
+
if (reused) log('INFO', `[TIMING:${chatId}] reusing warm pid=${child.pid} (+0ms)`);
|
|
788
|
+
else log('INFO', `[TIMING:${chatId}] spawned ${rt.name} pid=${child.pid}`);
|
|
835
789
|
|
|
836
790
|
if (chatId) {
|
|
837
|
-
activeProcesses
|
|
791
|
+
setActiveChildProcess(activeProcesses, saveActivePids, chatId, {
|
|
838
792
|
child,
|
|
839
793
|
aborted: false,
|
|
840
794
|
abortReason: null,
|
|
@@ -842,18 +796,16 @@ function createClaudeEngine(deps) {
|
|
|
842
796
|
engine: rt.name,
|
|
843
797
|
killSignal: rt.killSignal || 'SIGTERM',
|
|
844
798
|
});
|
|
845
|
-
saveActivePids();
|
|
846
799
|
}
|
|
847
800
|
|
|
848
801
|
let buffer = '';
|
|
849
802
|
let stderr = '';
|
|
850
|
-
let killed = false;
|
|
851
|
-
let killedReason = 'idle';
|
|
852
803
|
let finalResult = '';
|
|
853
804
|
let finalUsage = null;
|
|
854
805
|
let observedSessionId = '';
|
|
855
806
|
let _firstOutputLogged = false;
|
|
856
807
|
let classifiedError = null;
|
|
808
|
+
let stdinFailureError = null;
|
|
857
809
|
let lastStatusTime = 0;
|
|
858
810
|
const STATUS_THROTTLE = statusThrottleMs;
|
|
859
811
|
// Streaming card: accumulate text and push to card in real-time (throttled)
|
|
@@ -861,82 +813,195 @@ function createClaudeEngine(deps) {
|
|
|
861
813
|
let _lastStreamFlush = 0;
|
|
862
814
|
const STREAM_THROTTLE = 1500; // ms between card edits (safe within Feishu 5 req/s limit)
|
|
863
815
|
function flushStream(force) {
|
|
864
|
-
if (!onStatus
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
816
|
+
if (!onStatus) return;
|
|
817
|
+
const flush = buildStreamFlushPayload(
|
|
818
|
+
{ streamText: _streamText, lastFlushAt: _lastStreamFlush },
|
|
819
|
+
{ force, now: Date.now(), throttleMs: STREAM_THROTTLE }
|
|
820
|
+
);
|
|
821
|
+
if (!flush.shouldFlush) return;
|
|
822
|
+
_lastStreamFlush = flush.lastFlushAt;
|
|
823
|
+
onStatus(flush.payload).catch(() => { });
|
|
869
824
|
}
|
|
870
825
|
const writtenFiles = [];
|
|
871
826
|
const toolUsageLog = [];
|
|
872
827
|
|
|
873
|
-
void timeoutMs;
|
|
874
|
-
const engineTimeouts = rt.timeouts || {};
|
|
875
|
-
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs
|
|
876
|
-
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs
|
|
877
|
-
const HARD_CEILING_MS = engineTimeouts.ceilingMs
|
|
828
|
+
void timeoutMs; // positional placeholder — actual timeouts come from engine config
|
|
829
|
+
const engineTimeouts = resolveStreamingTimeouts(rt.timeouts || {});
|
|
830
|
+
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs;
|
|
831
|
+
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs;
|
|
832
|
+
const HARD_CEILING_MS = engineTimeouts.ceilingMs;
|
|
878
833
|
const startTime = Date.now();
|
|
879
834
|
let waitingForTool = false;
|
|
880
835
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
},
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
let idleTimer = setTimeout(() => killChild('idle'), IDLE_TIMEOUT_MS);
|
|
895
|
-
const ceilingTimer = setTimeout(() => killChild('ceiling'), HARD_CEILING_MS);
|
|
836
|
+
const watchdog = createStreamingWatchdog({
|
|
837
|
+
child,
|
|
838
|
+
killSignal: rt.killSignal || 'SIGTERM',
|
|
839
|
+
useProcessGroup: process.platform !== 'win32',
|
|
840
|
+
idleTimeoutMs: IDLE_TIMEOUT_MS,
|
|
841
|
+
toolTimeoutMs: TOOL_EXEC_TIMEOUT_MS,
|
|
842
|
+
ceilingTimeoutMs: HARD_CEILING_MS,
|
|
843
|
+
forceKillDelayMs: 5000,
|
|
844
|
+
onKill(reason) {
|
|
845
|
+
log('WARN', `[${rt.name}] ${reason} timeout for chatId ${chatId} — killing process group`);
|
|
846
|
+
},
|
|
847
|
+
});
|
|
896
848
|
|
|
897
|
-
function
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
849
|
+
function abortForStdinFailure(err) {
|
|
850
|
+
if (stdinFailureError) return;
|
|
851
|
+
stdinFailureError = err && err.message ? err.message : String(err || 'stdin error');
|
|
852
|
+
abortStreamingChildLifecycle({
|
|
853
|
+
child,
|
|
854
|
+
watchdog,
|
|
855
|
+
milestoneTimer,
|
|
856
|
+
activeProcesses,
|
|
857
|
+
saveActivePids,
|
|
858
|
+
chatId,
|
|
859
|
+
reason: 'stdin',
|
|
860
|
+
});
|
|
861
|
+
absorbBufferedEvents();
|
|
862
|
+
finalize(buildStreamingResult({
|
|
863
|
+
output: finalResult || null,
|
|
864
|
+
error: stdinFailureError,
|
|
865
|
+
files: writtenFiles,
|
|
866
|
+
toolUsageLog,
|
|
867
|
+
usage: finalUsage,
|
|
868
|
+
sessionId: observedSessionId || '',
|
|
869
|
+
}));
|
|
901
870
|
}
|
|
902
871
|
|
|
903
872
|
let toolCallCount = 0;
|
|
904
873
|
let lastMilestoneMin = 0;
|
|
905
874
|
const milestoneTimer = setInterval(() => {
|
|
906
|
-
if (
|
|
875
|
+
if (watchdog.isKilled()) return;
|
|
907
876
|
const elapsedMin = Math.floor((Date.now() - startTime) / 60000);
|
|
908
877
|
const nextMin = lastMilestoneMin === 0 ? 2 : lastMilestoneMin + 5;
|
|
909
878
|
if (elapsedMin >= nextMin) {
|
|
910
879
|
lastMilestoneMin = elapsedMin;
|
|
911
|
-
const parts = [`⏳ 已运行 ${elapsedMin} 分钟`];
|
|
912
|
-
if (toolCallCount > 0) parts.push(`调用 ${toolCallCount} 次工具`);
|
|
913
|
-
if (writtenFiles.length > 0) parts.push(`修改 ${writtenFiles.length} 个文件`);
|
|
914
|
-
const recentTool = toolUsageLog.length > 0 ? toolUsageLog[toolUsageLog.length - 1] : null;
|
|
915
|
-
if (recentTool) {
|
|
916
|
-
const ctx = recentTool.context || recentTool.skill || '';
|
|
917
|
-
parts.push(`最近: ${recentTool.tool}${ctx ? ' ' + ctx : ''}`);
|
|
918
|
-
}
|
|
919
880
|
if (onStatus) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
881
|
+
onStatus(buildMilestoneOverlayPayload({
|
|
882
|
+
elapsedMin,
|
|
883
|
+
toolCallCount,
|
|
884
|
+
writtenFiles,
|
|
885
|
+
toolUsageLog,
|
|
886
|
+
streamText: _streamText,
|
|
887
|
+
})).catch(() => { });
|
|
923
888
|
}
|
|
924
889
|
}
|
|
925
890
|
}, 30000);
|
|
926
891
|
|
|
927
892
|
function parseEventsFromLine(line) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
893
|
+
return parseStreamingEvents(rt.parseStreamEvent, line);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function applyContentState(event, buffered) {
|
|
897
|
+
const contentState = applyStreamingContentState(
|
|
898
|
+
{ finalResult, streamText: _streamText, waitingForTool, finalUsage },
|
|
899
|
+
event
|
|
900
|
+
);
|
|
901
|
+
finalResult = contentState.finalResult;
|
|
902
|
+
_streamText = contentState.streamText;
|
|
903
|
+
waitingForTool = contentState.waitingForTool;
|
|
904
|
+
finalUsage = contentState.finalUsage;
|
|
905
|
+
if (!buffered && contentState.shouldUpdateWatchdog) watchdog.setWaitingForTool(contentState.watchdogWaiting);
|
|
906
|
+
if (!buffered && contentState.shouldFlush) flushStream(contentState.flushForce);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function applyStreamEvent(event, options = {}) {
|
|
910
|
+
if (!event || !event.type) return;
|
|
911
|
+
|
|
912
|
+
const buffered = options.buffered === true;
|
|
913
|
+
if (event.type === 'session' && event.sessionId) {
|
|
914
|
+
observedSessionId = applyStreamingMetadata(
|
|
915
|
+
{ observedSessionId, classifiedError },
|
|
916
|
+
event
|
|
917
|
+
).observedSessionId;
|
|
918
|
+
if (!buffered && typeof onSession === 'function') {
|
|
919
|
+
Promise.resolve(onSession(observedSessionId)).catch(() => { });
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (event.type === 'error') {
|
|
924
|
+
classifiedError = applyStreamingMetadata(
|
|
925
|
+
{ observedSessionId, classifiedError },
|
|
926
|
+
event
|
|
927
|
+
).classifiedError;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (event.type === 'text' && event.text) {
|
|
931
|
+
applyContentState(event, buffered);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (event.type === 'done') {
|
|
935
|
+
applyContentState(event, buffered);
|
|
936
|
+
|
|
937
|
+
if (!buffered && isPersistent) {
|
|
938
|
+
finalize(finalizePersistentStreamingTurn({
|
|
939
|
+
watchdog,
|
|
940
|
+
milestoneTimer,
|
|
941
|
+
activeProcesses,
|
|
942
|
+
saveActivePids,
|
|
943
|
+
chatId,
|
|
944
|
+
warmPool: _warmPool,
|
|
945
|
+
warmSessionKey,
|
|
946
|
+
child,
|
|
947
|
+
observedSessionId,
|
|
948
|
+
cwd,
|
|
949
|
+
output: finalResult || '',
|
|
950
|
+
files: writtenFiles,
|
|
951
|
+
toolUsageLog,
|
|
952
|
+
usage: finalUsage,
|
|
953
|
+
}));
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (event.type !== 'tool_result' && event.type !== 'tool_use') return;
|
|
958
|
+
|
|
959
|
+
const toolState = applyStreamingToolState(
|
|
960
|
+
{ waitingForTool, toolCallCount, toolUsageLog, writtenFiles },
|
|
961
|
+
event,
|
|
962
|
+
{ pathModule: path, maxEntries: 50 }
|
|
963
|
+
);
|
|
964
|
+
toolCallCount = toolState.toolCallCount;
|
|
965
|
+
waitingForTool = toolState.waitingForTool;
|
|
966
|
+
toolUsageLog.length = 0;
|
|
967
|
+
toolUsageLog.push(...toolState.toolUsageLog);
|
|
968
|
+
writtenFiles.length = 0;
|
|
969
|
+
writtenFiles.push(...toolState.writtenFiles);
|
|
970
|
+
if (!buffered && toolState.shouldUpdateWatchdog) watchdog.setWaitingForTool(toolState.watchdogWaiting);
|
|
971
|
+
|
|
972
|
+
if (event.type !== 'tool_use' || buffered) return;
|
|
973
|
+
|
|
974
|
+
const overlay = buildToolOverlayPayload({
|
|
975
|
+
toolName: toolState.toolName,
|
|
976
|
+
toolInput: toolState.toolInput,
|
|
977
|
+
streamText: _streamText,
|
|
978
|
+
lastStatusTime,
|
|
979
|
+
now: Date.now(),
|
|
980
|
+
throttleMs: STATUS_THROTTLE,
|
|
981
|
+
toolEmoji: TOOL_EMOJI,
|
|
982
|
+
pathModule: path,
|
|
983
|
+
});
|
|
984
|
+
if (!overlay.shouldEmit) return;
|
|
985
|
+
lastStatusTime = overlay.lastStatusTime;
|
|
986
|
+
if (onStatus) {
|
|
987
|
+
onStatus(overlay.payload).catch(() => { });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function absorbBufferedEvents() {
|
|
992
|
+
if (!buffer.trim()) return;
|
|
993
|
+
const events = parseEventsFromLine(buffer.trim());
|
|
994
|
+
buffer = '';
|
|
995
|
+
for (const event of events) {
|
|
996
|
+
applyStreamEvent(event, { buffered: true });
|
|
932
997
|
}
|
|
933
998
|
}
|
|
934
999
|
|
|
935
1000
|
child.stdout.on('data', (data) => {
|
|
936
|
-
|
|
937
|
-
buffer
|
|
938
|
-
const lines =
|
|
939
|
-
buffer =
|
|
1001
|
+
watchdog.resetIdle();
|
|
1002
|
+
const stdoutState = splitStreamingStdoutChunk(buffer, data.toString());
|
|
1003
|
+
const lines = stdoutState.lines;
|
|
1004
|
+
buffer = stdoutState.buffer;
|
|
940
1005
|
|
|
941
1006
|
for (const line of lines) {
|
|
942
1007
|
if (!line.trim()) continue;
|
|
@@ -946,260 +1011,113 @@ function createClaudeEngine(deps) {
|
|
|
946
1011
|
}
|
|
947
1012
|
const events = parseEventsFromLine(line);
|
|
948
1013
|
for (const event of events) {
|
|
949
|
-
|
|
950
|
-
if (event.type === 'session' && event.sessionId) {
|
|
951
|
-
observedSessionId = String(event.sessionId);
|
|
952
|
-
if (typeof onSession === 'function') {
|
|
953
|
-
Promise.resolve(onSession(observedSessionId)).catch(() => { });
|
|
954
|
-
}
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
if (event.type === 'error') {
|
|
958
|
-
classifiedError = event;
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
if (event.type === 'text' && event.text) {
|
|
962
|
-
finalResult += (finalResult ? '\n\n' : '') + String(event.text);
|
|
963
|
-
_streamText = finalResult;
|
|
964
|
-
if (waitingForTool) {
|
|
965
|
-
waitingForTool = false;
|
|
966
|
-
resetIdleTimer();
|
|
967
|
-
}
|
|
968
|
-
flushStream(); // throttled stream to card
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
if (event.type === 'done') {
|
|
972
|
-
finalUsage = event.usage || null;
|
|
973
|
-
if (waitingForTool) {
|
|
974
|
-
waitingForTool = false;
|
|
975
|
-
resetIdleTimer();
|
|
976
|
-
}
|
|
977
|
-
// Fallback: if no text streamed yet (tool-only response), use result text from done.
|
|
978
|
-
// Do NOT use when finalResult already has content — result duplicates streamed text.
|
|
979
|
-
if (!finalResult && event.result) {
|
|
980
|
-
finalResult = String(event.result);
|
|
981
|
-
_streamText = finalResult;
|
|
982
|
-
}
|
|
983
|
-
flushStream(true); // force final text flush before process ends
|
|
984
|
-
|
|
985
|
-
// Persistent mode: finalize on result event, keep process alive for reuse
|
|
986
|
-
if (isPersistent) {
|
|
987
|
-
clearTimeout(idleTimer);
|
|
988
|
-
clearTimeout(ceilingTimer);
|
|
989
|
-
clearTimeout(sigkillTimer);
|
|
990
|
-
clearInterval(milestoneTimer);
|
|
991
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
992
|
-
// Store process back in warm pool for next turn
|
|
993
|
-
if (_warmPool && warmSessionKey && child && !child.killed && child.exitCode === null) {
|
|
994
|
-
_warmPool.storeWarm(warmSessionKey, child, { sessionId: observedSessionId, cwd });
|
|
995
|
-
}
|
|
996
|
-
finalize({
|
|
997
|
-
output: finalResult || '',
|
|
998
|
-
error: null,
|
|
999
|
-
files: writtenFiles,
|
|
1000
|
-
toolUsageLog,
|
|
1001
|
-
usage: finalUsage,
|
|
1002
|
-
sessionId: observedSessionId || '',
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
if (event.type === 'tool_result') {
|
|
1008
|
-
if (waitingForTool) {
|
|
1009
|
-
waitingForTool = false;
|
|
1010
|
-
resetIdleTimer();
|
|
1011
|
-
}
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
if (event.type !== 'tool_use') continue;
|
|
1015
|
-
|
|
1016
|
-
toolCallCount++;
|
|
1017
|
-
waitingForTool = true;
|
|
1018
|
-
resetIdleTimer();
|
|
1019
|
-
const toolName = event.toolName || 'Tool';
|
|
1020
|
-
const toolInput = event.toolInput || {};
|
|
1021
|
-
|
|
1022
|
-
const toolEntry = { tool: toolName };
|
|
1023
|
-
if (toolName === 'Skill' && toolInput.skill) toolEntry.skill = toolInput.skill;
|
|
1024
|
-
else if (toolInput.command) toolEntry.context = String(toolInput.command).slice(0, 50);
|
|
1025
|
-
else if (toolInput.file_path) toolEntry.context = path.basename(String(toolInput.file_path));
|
|
1026
|
-
if (toolUsageLog.length < 50) toolUsageLog.push(toolEntry);
|
|
1027
|
-
|
|
1028
|
-
if (toolName === 'Write' && toolInput.file_path) {
|
|
1029
|
-
const filePath = String(toolInput.file_path);
|
|
1030
|
-
if (!writtenFiles.includes(filePath)) writtenFiles.push(filePath);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const now = Date.now();
|
|
1034
|
-
if (now - lastStatusTime < STATUS_THROTTLE) continue;
|
|
1035
|
-
lastStatusTime = now;
|
|
1036
|
-
|
|
1037
|
-
const emoji = TOOL_EMOJI[toolName] || TOOL_EMOJI.default;
|
|
1038
|
-
let displayName = toolName;
|
|
1039
|
-
let displayEmoji = emoji;
|
|
1040
|
-
let context = '';
|
|
1041
|
-
|
|
1042
|
-
if (toolName === 'Skill' && toolInput.skill) {
|
|
1043
|
-
context = toolInput.skill;
|
|
1044
|
-
} else if ((toolName === 'Task' || toolName === 'Agent') && toolInput.description) {
|
|
1045
|
-
const agentType = toolInput.subagent_type ? `[${toolInput.subagent_type}] ` : '';
|
|
1046
|
-
context = (agentType + String(toolInput.description)).slice(0, 40);
|
|
1047
|
-
} else if (toolName.startsWith('mcp__')) {
|
|
1048
|
-
const parts = toolName.split('__');
|
|
1049
|
-
const server = parts[1] || 'unknown';
|
|
1050
|
-
const action = parts.slice(2).join('_') || '';
|
|
1051
|
-
if (server === 'playwright') {
|
|
1052
|
-
displayEmoji = '🌐';
|
|
1053
|
-
displayName = 'Browser';
|
|
1054
|
-
context = action.replace(/_/g, ' ');
|
|
1055
|
-
} else {
|
|
1056
|
-
displayEmoji = '🔗';
|
|
1057
|
-
displayName = `MCP:${server}`;
|
|
1058
|
-
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
1059
|
-
}
|
|
1060
|
-
} else if (toolInput.file_path) {
|
|
1061
|
-
const basename = path.basename(String(toolInput.file_path));
|
|
1062
|
-
const dotIdx = basename.lastIndexOf('.');
|
|
1063
|
-
context = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
1064
|
-
} else if (toolInput.command) {
|
|
1065
|
-
context = String(toolInput.command).slice(0, 30);
|
|
1066
|
-
if (String(toolInput.command).length > 30) context += '...';
|
|
1067
|
-
} else if (toolInput.pattern) {
|
|
1068
|
-
context = String(toolInput.pattern).slice(0, 20);
|
|
1069
|
-
} else if (toolInput.query) {
|
|
1070
|
-
context = String(toolInput.query).slice(0, 25);
|
|
1071
|
-
} else if (toolInput.url) {
|
|
1072
|
-
try { context = new URL(toolInput.url).hostname; } catch { context = 'web'; }
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const status = context
|
|
1076
|
-
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
1077
|
-
: `${displayEmoji} ${displayName}...`;
|
|
1078
|
-
if (onStatus) {
|
|
1079
|
-
// Overlay tool status on top of streamed text (if any); else show plain status
|
|
1080
|
-
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${status}` : status;
|
|
1081
|
-
onStatus(msg).catch(() => { });
|
|
1082
|
-
}
|
|
1014
|
+
applyStreamEvent(event);
|
|
1083
1015
|
}
|
|
1084
1016
|
}
|
|
1085
1017
|
});
|
|
1086
1018
|
|
|
1087
1019
|
child.stderr.on('data', (data) => {
|
|
1088
|
-
|
|
1020
|
+
watchdog.resetIdle();
|
|
1089
1021
|
const chunk = data.toString();
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1022
|
+
const stderrState = accumulateStreamingStderr(
|
|
1023
|
+
{ stderr, classifiedError },
|
|
1024
|
+
chunk,
|
|
1025
|
+
{ classifyError: rt.classifyError }
|
|
1026
|
+
);
|
|
1027
|
+
stderr = stderrState.stderr;
|
|
1028
|
+
classifiedError = stderrState.classifiedError;
|
|
1029
|
+
if (stderrState.isApiError) {
|
|
1093
1030
|
log('ERROR', `[API-ERROR] ${rt.name} stderr for ${chatId}: ${chunk.slice(0, 300)}`);
|
|
1094
1031
|
}
|
|
1095
|
-
if (!classifiedError && typeof rt.classifyError === 'function') {
|
|
1096
|
-
classifiedError = rt.classifyError(chunk);
|
|
1097
|
-
}
|
|
1098
1032
|
});
|
|
1099
1033
|
|
|
1034
|
+
if (child.stdin && typeof child.stdin.on === 'function') {
|
|
1035
|
+
child.stdin.on('error', (err) => {
|
|
1036
|
+
abortForStdinFailure(err);
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1100
1040
|
child.on('close', (code) => {
|
|
1101
1041
|
log('INFO', `[TIMING:${chatId}] process-close code=${code} total=${Date.now() - _spawnAt}ms`);
|
|
1102
|
-
|
|
1103
|
-
clearTimeout(ceilingTimer);
|
|
1104
|
-
clearTimeout(sigkillTimer);
|
|
1105
|
-
clearInterval(milestoneTimer);
|
|
1042
|
+
stopStreamingLifecycle(watchdog, milestoneTimer);
|
|
1106
1043
|
|
|
1107
1044
|
// Persistent mode: if already finalized on result event, just clean up
|
|
1108
1045
|
if (isPersistent && settled) {
|
|
1109
|
-
|
|
1046
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
1110
1047
|
// Process died after we returned result — remove from warm pool
|
|
1111
1048
|
if (_warmPool && warmSessionKey) _warmPool.releaseWarm(warmSessionKey);
|
|
1112
1049
|
return;
|
|
1113
1050
|
}
|
|
1114
1051
|
|
|
1115
|
-
|
|
1116
|
-
const events = parseEventsFromLine(buffer.trim());
|
|
1117
|
-
for (const event of events) {
|
|
1118
|
-
if (event.type === 'text' && event.text) finalResult = String(event.text);
|
|
1119
|
-
if (event.type === 'done') finalUsage = event.usage || null;
|
|
1120
|
-
if (event.type === 'session' && event.sessionId) observedSessionId = String(event.sessionId);
|
|
1121
|
-
if (event.type === 'error') classifiedError = event;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1052
|
+
absorbBufferedEvents();
|
|
1124
1053
|
|
|
1125
1054
|
const proc = chatId ? activeProcesses.get(chatId) : null;
|
|
1126
1055
|
const wasAborted = proc && proc.aborted;
|
|
1127
1056
|
const abortReason = proc && proc.abortReason ? String(proc.abortReason) : '';
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
if (killed) {
|
|
1148
|
-
const elapsed = Math.round((Date.now() - startTime) / 60000);
|
|
1149
|
-
const idleMin = Math.max(1, Math.round(IDLE_TIMEOUT_MS / 60000));
|
|
1150
|
-
const reason = killedReason === 'ceiling'
|
|
1151
|
-
? `⏱ 已运行 ${elapsed} 分钟,达到上限(${Math.round(HARD_CEILING_MS / 60000)} 分钟)`
|
|
1152
|
-
: `⏱ 已 ${idleMin} 分钟无输出,判定卡死(共运行 ${elapsed} 分钟)`;
|
|
1153
|
-
finalize({ output: finalResult || null, error: reason, timedOut: true, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (code !== 0) {
|
|
1157
|
-
const engineErr = classifiedError && classifiedError.message
|
|
1158
|
-
? classifiedError.message
|
|
1159
|
-
: (stderr || `Exit code ${code}`);
|
|
1160
|
-
finalize({ output: finalResult || null, error: engineErr, errorCode: classifiedError ? classifiedError.code : undefined, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
finalize({ output: finalResult || '', error: null, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
1057
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
1058
|
+
finalize(resolveStreamingClosePayload({
|
|
1059
|
+
code,
|
|
1060
|
+
streamState: { finalResult, finalUsage, observedSessionId, writtenFiles, toolUsageLog },
|
|
1061
|
+
wasAborted,
|
|
1062
|
+
abortReason,
|
|
1063
|
+
stdinFailureError,
|
|
1064
|
+
watchdog,
|
|
1065
|
+
timeoutConfig: {
|
|
1066
|
+
startTime,
|
|
1067
|
+
idleTimeoutMs: IDLE_TIMEOUT_MS,
|
|
1068
|
+
toolTimeoutMs: TOOL_EXEC_TIMEOUT_MS,
|
|
1069
|
+
hardCeilingMs: HARD_CEILING_MS,
|
|
1070
|
+
formatTimeoutWindowLabel,
|
|
1071
|
+
},
|
|
1072
|
+
classifiedError,
|
|
1073
|
+
stderr,
|
|
1074
|
+
}));
|
|
1164
1075
|
});
|
|
1165
1076
|
|
|
1166
1077
|
child.on('error', (err) => {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
clearTimeout(sigkillTimer);
|
|
1170
|
-
clearInterval(milestoneTimer);
|
|
1171
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
1078
|
+
stopStreamingLifecycle(watchdog, milestoneTimer);
|
|
1079
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
1172
1080
|
finalize({ output: null, error: formatEngineSpawnError(err, rt), files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
1173
1081
|
});
|
|
1174
1082
|
|
|
1175
1083
|
try {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1084
|
+
writeStreamingChildInput({
|
|
1085
|
+
child,
|
|
1086
|
+
input,
|
|
1087
|
+
isPersistent,
|
|
1088
|
+
warmPool: _warmPool,
|
|
1089
|
+
observedSessionId,
|
|
1090
|
+
});
|
|
1183
1091
|
} catch (e) {
|
|
1184
|
-
|
|
1185
|
-
clearTimeout(ceilingTimer);
|
|
1186
|
-
clearTimeout(sigkillTimer);
|
|
1187
|
-
clearInterval(milestoneTimer);
|
|
1188
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
1189
|
-
try { child.stdin.destroy(); } catch { /* ignore */ }
|
|
1190
|
-
try {
|
|
1191
|
-
const sig = rt.killSignal || 'SIGTERM';
|
|
1192
|
-
process.kill(-child.pid, sig);
|
|
1193
|
-
} catch {
|
|
1194
|
-
try { child.kill(rt.killSignal || 'SIGTERM'); } catch { /* ignore */ }
|
|
1195
|
-
}
|
|
1196
|
-
finalize({ output: null, error: e.message, files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
1092
|
+
abortForStdinFailure(e);
|
|
1197
1093
|
}
|
|
1198
1094
|
});
|
|
1199
1095
|
}
|
|
1200
1096
|
|
|
1097
|
+
const MSG_SESSION_MAX_ENTRIES = 5000;
|
|
1098
|
+
const MSG_SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
1099
|
+
|
|
1100
|
+
function pruneMsgSessionMappings(msgSessions) {
|
|
1101
|
+
const now = Date.now();
|
|
1102
|
+
const entries = Object.entries(msgSessions || {});
|
|
1103
|
+
if (entries.length === 0) return {};
|
|
1104
|
+
|
|
1105
|
+
const freshEntries = entries.filter(([, value]) => {
|
|
1106
|
+
const touchedAt = Number(value && value.touchedAt || 0);
|
|
1107
|
+
return !touchedAt || (now - touchedAt) <= MSG_SESSION_MAX_AGE_MS;
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const boundedEntries = freshEntries.length > MSG_SESSION_MAX_ENTRIES
|
|
1111
|
+
? freshEntries
|
|
1112
|
+
.sort((a, b) => Number((a[1] && a[1].touchedAt) || 0) - Number((b[1] && b[1].touchedAt) || 0))
|
|
1113
|
+
.slice(freshEntries.length - MSG_SESSION_MAX_ENTRIES)
|
|
1114
|
+
: freshEntries;
|
|
1115
|
+
return Object.fromEntries(boundedEntries);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1201
1118
|
// Track outbound message_id → session for reply-based session restoration.
|
|
1202
|
-
//
|
|
1119
|
+
// Keep a larger, time-bounded mapping pool so active chats do not lose
|
|
1120
|
+
// reply continuity after a few hundred messages across all groups.
|
|
1203
1121
|
function trackMsgSession(messageId, session, agentKey, options = {}) {
|
|
1204
1122
|
if (!messageId || !session) return;
|
|
1205
1123
|
const forceRouteOnly = !!(options && options.routeOnly);
|
|
@@ -1215,11 +1133,9 @@ function createClaudeEngine(deps) {
|
|
|
1215
1133
|
...(session.sandboxMode ? { sandboxMode: session.sandboxMode } : {}),
|
|
1216
1134
|
...(session.approvalPolicy ? { approvalPolicy: session.approvalPolicy } : {}),
|
|
1217
1135
|
...(session.permissionMode ? { permissionMode: session.permissionMode } : {}),
|
|
1136
|
+
touchedAt: Date.now(),
|
|
1218
1137
|
};
|
|
1219
|
-
|
|
1220
|
-
if (keys.length > 200) {
|
|
1221
|
-
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
1222
|
-
}
|
|
1138
|
+
st.msg_sessions = pruneMsgSessionMappings(st.msg_sessions);
|
|
1223
1139
|
saveState(st);
|
|
1224
1140
|
}
|
|
1225
1141
|
|
|
@@ -1232,18 +1148,14 @@ function createClaudeEngine(deps) {
|
|
|
1232
1148
|
* Reset active provider back to anthropic/opus and reload config.
|
|
1233
1149
|
* Returns the freshly loaded config so callers can reassign their local variable.
|
|
1234
1150
|
*/
|
|
1235
|
-
function fallbackToDefaultProvider(reason
|
|
1151
|
+
function fallbackToDefaultProvider(reason) {
|
|
1236
1152
|
log('WARN', `Falling back to anthropic/opus — reason: ${reason}`);
|
|
1237
1153
|
if (providerMod && providerMod.getActiveName() !== 'anthropic') {
|
|
1238
1154
|
providerMod.setActive('anthropic');
|
|
1239
1155
|
}
|
|
1240
1156
|
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
1241
1157
|
if (!cfg.daemon) cfg.daemon = {};
|
|
1242
|
-
|
|
1243
|
-
cfg.daemon.models.claude = 'opus';
|
|
1244
|
-
if (boundProjectKey && cfg.projects && cfg.projects[boundProjectKey]) {
|
|
1245
|
-
cfg.projects[boundProjectKey].model = 'opus';
|
|
1246
|
-
}
|
|
1158
|
+
cfg.daemon.model = 'opus';
|
|
1247
1159
|
writeConfigSafe(cfg);
|
|
1248
1160
|
return loadConfig();
|
|
1249
1161
|
}
|
|
@@ -1258,7 +1170,7 @@ function createClaudeEngine(deps) {
|
|
|
1258
1170
|
const _existing = activeProcesses.get(chatId);
|
|
1259
1171
|
if (_existing && _existing.child && !_existing.aborted) {
|
|
1260
1172
|
log('WARN', `askClaude: overwriting active process for ${chatId} — aborting previous`);
|
|
1261
|
-
|
|
1173
|
+
terminateChildProcess(_existing.child, 'SIGTERM', { useProcessGroup: process.platform !== 'win32' });
|
|
1262
1174
|
}
|
|
1263
1175
|
activeProcesses.set(chatId, {
|
|
1264
1176
|
child: null, // sentinel: no process yet
|
|
@@ -1290,7 +1202,7 @@ function createClaudeEngine(deps) {
|
|
|
1290
1202
|
...(config.feishu ? config.feishu.chat_agent_map || {} : {}),
|
|
1291
1203
|
...(config.imessage ? config.imessage.chat_agent_map || {} : {}),
|
|
1292
1204
|
};
|
|
1293
|
-
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
1205
|
+
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || _ackAgentMap[rawChatId(_ackChatIdStr)] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
1294
1206
|
const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
|
|
1295
1207
|
// _ackCardHeader: non-null for bound projects with a name; passed to editMessage to preserve header on streaming edits
|
|
1296
1208
|
let _ackCardHeader = (_ackBoundProj && _ackBoundProj.name)
|
|
@@ -1343,7 +1255,7 @@ function createClaudeEngine(deps) {
|
|
|
1343
1255
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1344
1256
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1345
1257
|
};
|
|
1346
|
-
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
1258
|
+
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || _strictAgentMap[rawChatId(String(chatId))] || projectKeyFromVirtualChatId(String(chatId)));
|
|
1347
1259
|
const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
|
|
1348
1260
|
if (agentMatch) {
|
|
1349
1261
|
const { key, proj, rest } = agentMatch;
|
|
@@ -1372,7 +1284,7 @@ function createClaudeEngine(deps) {
|
|
|
1372
1284
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1373
1285
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1374
1286
|
};
|
|
1375
|
-
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
1287
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || chatAgentMap[rawChatId(chatIdStr)] || projectKeyFromVirtualChatId(chatIdStr);
|
|
1376
1288
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
1377
1289
|
const daemonCfg = (config && config.daemon) || {};
|
|
1378
1290
|
// Keep real group chats on their own session key.
|
|
@@ -1569,7 +1481,7 @@ function createClaudeEngine(deps) {
|
|
|
1569
1481
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1570
1482
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1571
1483
|
};
|
|
1572
|
-
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1484
|
+
const projectKey = _agentMap0[_cid0] || _agentMap0[rawChatId(_cid0)] || projectKeyFromVirtualChatId(_cid0);
|
|
1573
1485
|
try {
|
|
1574
1486
|
const memory = require('./memory');
|
|
1575
1487
|
|
|
@@ -1752,6 +1664,25 @@ ${mentorRadarHint}
|
|
|
1752
1664
|
6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
|
|
1753
1665
|
}
|
|
1754
1666
|
|
|
1667
|
+
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
1668
|
+
let summaryHint = '';
|
|
1669
|
+
if (session.started) {
|
|
1670
|
+
try {
|
|
1671
|
+
const _stSum = loadState();
|
|
1672
|
+
const _sess = _stSum.sessions && _stSum.sessions[chatId];
|
|
1673
|
+
if (_sess && _sess.last_summary && _sess.last_summary_at) {
|
|
1674
|
+
const _idleMs = Date.now() - (_sess.last_active || 0);
|
|
1675
|
+
const _summaryAgeH = (Date.now() - _sess.last_summary_at) / 3600000;
|
|
1676
|
+
if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
|
|
1677
|
+
summaryHint = `
|
|
1678
|
+
|
|
1679
|
+
[上次对话摘要(历史已完成,仅供上下文,请勿重复执行)]: ${_sess.last_summary}`;
|
|
1680
|
+
log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
} catch { /* non-critical */ }
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1755
1686
|
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1756
1687
|
let mentorHint = '';
|
|
1757
1688
|
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
@@ -1823,7 +1754,7 @@ ${mentorRadarHint}
|
|
|
1823
1754
|
// (varies per prompt), so include it even on warm reuse.
|
|
1824
1755
|
const fullPrompt = _warmEntry
|
|
1825
1756
|
? routedPrompt + intentHint
|
|
1826
|
-
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + memoryHint + mentorHint + langGuard;
|
|
1757
|
+
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
1827
1758
|
if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
|
|
1828
1759
|
const actualPermissionProfile = getActualCodexPermissionProfile(session);
|
|
1829
1760
|
if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
|
|
@@ -2114,10 +2045,10 @@ ${mentorRadarHint}
|
|
|
2114
2045
|
markCodexResumeRetried(chatId, resumeFailure.kind);
|
|
2115
2046
|
log(
|
|
2116
2047
|
'WARN',
|
|
2117
|
-
`Codex resume failed for ${chatId}, retrying once with ${resumeFailure.kind === 'interrupted' ? 'native resume recovery' : 'fresh exec'}: ${String(error).slice(0, 120)}`
|
|
2048
|
+
`Codex resume failed for ${chatId}, retrying once with ${(resumeFailure.kind === 'interrupted' || resumeFailure.kind === 'transport') ? 'native resume recovery' : 'fresh exec'}: ${String(error).slice(0, 120)}`
|
|
2118
2049
|
);
|
|
2119
2050
|
await bot.sendMessage(chatId, resumeFailure.userMessage).catch(() => { });
|
|
2120
|
-
if (resumeFailure.kind !== 'interrupted') {
|
|
2051
|
+
if (resumeFailure.kind !== 'interrupted' && resumeFailure.kind !== 'transport') {
|
|
2121
2052
|
session = createSession(
|
|
2122
2053
|
sessionChatId,
|
|
2123
2054
|
effectiveCwd,
|
|
@@ -2251,7 +2182,7 @@ ${mentorRadarHint}
|
|
|
2251
2182
|
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
2252
2183
|
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
2253
2184
|
try {
|
|
2254
|
-
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}
|
|
2185
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
2255
2186
|
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
2256
2187
|
} catch (fbErr) {
|
|
2257
2188
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
@@ -2432,13 +2363,26 @@ ${mentorRadarHint}
|
|
|
2432
2363
|
return { ok: false, error: errMsg, errorCode };
|
|
2433
2364
|
}
|
|
2434
2365
|
|
|
2435
|
-
// If session not found / locked / thinking signature invalid — create new and retry once (Claude path)
|
|
2366
|
+
// If session not found / locked / thinking signature invalid — try repair or create new and retry once (Claude path)
|
|
2436
2367
|
const _isThinkingSignatureError = isClaudeThinkingSignatureError(errMsg);
|
|
2437
2368
|
const _isSessionResumeFail = errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use') || _isThinkingSignatureError;
|
|
2438
2369
|
if (runtime.name === 'claude' && _isSessionResumeFail) {
|
|
2439
2370
|
const _reason = errMsg.includes('already in use') ? 'locked' : _isThinkingSignatureError ? 'thinking-signature-invalid' : 'not found';
|
|
2440
|
-
|
|
2441
|
-
|
|
2371
|
+
|
|
2372
|
+
// For thinking signature errors, try to repair the session in-place first (preserve context)
|
|
2373
|
+
let _repaired = false;
|
|
2374
|
+
if (_isThinkingSignatureError && session.id) {
|
|
2375
|
+
const stripped = stripThinkingSignatures(session.id);
|
|
2376
|
+
if (stripped > 0) {
|
|
2377
|
+
log('INFO', `Session ${session.id} repaired: stripped ${stripped} thinking signatures, retrying same session`);
|
|
2378
|
+
_repaired = true;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (!_repaired) {
|
|
2383
|
+
log('WARN', `Session ${session.id} unusable (${_reason}), creating new`);
|
|
2384
|
+
session = createSession(sessionChatId, effectiveCwd, '', runtime.name);
|
|
2385
|
+
}
|
|
2442
2386
|
|
|
2443
2387
|
const retryArgs = runtime.buildArgs({
|
|
2444
2388
|
model,
|
|
@@ -2482,7 +2426,7 @@ ${mentorRadarHint}
|
|
|
2482
2426
|
const builtinModelValues = (ENGINE_MODEL_CONFIG.claude.options || []).map(o => typeof o === 'string' ? o : o.value);
|
|
2483
2427
|
if ((activeProv !== 'anthropic' || !builtinModelValues.includes(model)) && !errMsg.includes('Stopped by user')) {
|
|
2484
2428
|
try {
|
|
2485
|
-
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}
|
|
2429
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
2486
2430
|
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
2487
2431
|
} catch (fallbackErr) {
|
|
2488
2432
|
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
@@ -2520,6 +2464,8 @@ ${mentorRadarHint}
|
|
|
2520
2464
|
_private: {
|
|
2521
2465
|
patchSessionSerialized,
|
|
2522
2466
|
shouldRetryCodexResumeFallback,
|
|
2467
|
+
resolveStreamingTimeouts,
|
|
2468
|
+
formatTimeoutWindowLabel,
|
|
2523
2469
|
formatEngineSpawnError,
|
|
2524
2470
|
adaptDaemonHintForEngine,
|
|
2525
2471
|
getSessionChatId,
|