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
|
@@ -6,11 +6,13 @@ const {
|
|
|
6
6
|
createEngineRuntimeFactory,
|
|
7
7
|
normalizeEngineName,
|
|
8
8
|
resolveEngineModel,
|
|
9
|
-
_private: { resolveCodexPermissionProfile, resolveEngineTimeouts },
|
|
10
9
|
ENGINE_MODEL_CONFIG,
|
|
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,48 +775,37 @@ 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
|
|
838
|
-
chatId,
|
|
791
|
+
setActiveChildProcess(activeProcesses, saveActivePids, chatId, {
|
|
839
792
|
child,
|
|
840
793
|
aborted: false,
|
|
841
794
|
abortReason: null,
|
|
842
795
|
startedAt: _spawnAt,
|
|
843
796
|
engine: rt.name,
|
|
844
797
|
killSignal: rt.killSignal || 'SIGTERM',
|
|
845
|
-
reactiveProjectKey: String(options && options.reactiveProjectKey || '').trim(),
|
|
846
798
|
});
|
|
847
|
-
saveActivePids();
|
|
848
799
|
}
|
|
849
800
|
|
|
850
801
|
let buffer = '';
|
|
851
802
|
let stderr = '';
|
|
852
|
-
let killed = false;
|
|
853
|
-
let killedReason = 'idle';
|
|
854
803
|
let finalResult = '';
|
|
855
804
|
let finalUsage = null;
|
|
856
805
|
let observedSessionId = '';
|
|
857
806
|
let _firstOutputLogged = false;
|
|
858
807
|
let classifiedError = null;
|
|
808
|
+
let stdinFailureError = null;
|
|
859
809
|
let lastStatusTime = 0;
|
|
860
810
|
const STATUS_THROTTLE = statusThrottleMs;
|
|
861
811
|
// Streaming card: accumulate text and push to card in real-time (throttled)
|
|
@@ -863,84 +813,195 @@ function createClaudeEngine(deps) {
|
|
|
863
813
|
let _lastStreamFlush = 0;
|
|
864
814
|
const STREAM_THROTTLE = 1500; // ms between card edits (safe within Feishu 5 req/s limit)
|
|
865
815
|
function flushStream(force) {
|
|
866
|
-
if (!onStatus
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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(() => { });
|
|
871
824
|
}
|
|
872
825
|
const writtenFiles = [];
|
|
873
826
|
const toolUsageLog = [];
|
|
874
827
|
|
|
875
|
-
void timeoutMs;
|
|
876
|
-
const engineTimeouts =
|
|
877
|
-
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs
|
|
878
|
-
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs
|
|
879
|
-
const HARD_CEILING_MS =
|
|
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;
|
|
880
833
|
const startTime = Date.now();
|
|
881
834
|
let waitingForTool = false;
|
|
882
835
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
},
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
let idleTimer = setTimeout(() => killChild('idle'), IDLE_TIMEOUT_MS);
|
|
897
|
-
const ceilingTimer = HARD_CEILING_MS && HARD_CEILING_MS > 0
|
|
898
|
-
? setTimeout(() => killChild('ceiling'), HARD_CEILING_MS)
|
|
899
|
-
: null;
|
|
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
|
+
});
|
|
900
848
|
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
+
}));
|
|
905
870
|
}
|
|
906
871
|
|
|
907
872
|
let toolCallCount = 0;
|
|
908
873
|
let lastMilestoneMin = 0;
|
|
909
874
|
const milestoneTimer = setInterval(() => {
|
|
910
|
-
if (
|
|
875
|
+
if (watchdog.isKilled()) return;
|
|
911
876
|
const elapsedMin = Math.floor((Date.now() - startTime) / 60000);
|
|
912
877
|
const nextMin = lastMilestoneMin === 0 ? 2 : lastMilestoneMin + 5;
|
|
913
878
|
if (elapsedMin >= nextMin) {
|
|
914
879
|
lastMilestoneMin = elapsedMin;
|
|
915
|
-
const parts = [`⏳ 已运行 ${elapsedMin} 分钟`];
|
|
916
|
-
if (toolCallCount > 0) parts.push(`调用 ${toolCallCount} 次工具`);
|
|
917
|
-
if (writtenFiles.length > 0) parts.push(`修改 ${writtenFiles.length} 个文件`);
|
|
918
|
-
const recentTool = toolUsageLog.length > 0 ? toolUsageLog[toolUsageLog.length - 1] : null;
|
|
919
|
-
if (recentTool) {
|
|
920
|
-
const ctx = recentTool.context || recentTool.skill || '';
|
|
921
|
-
parts.push(`最近: ${recentTool.tool}${ctx ? ' ' + ctx : ''}`);
|
|
922
|
-
}
|
|
923
880
|
if (onStatus) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
881
|
+
onStatus(buildMilestoneOverlayPayload({
|
|
882
|
+
elapsedMin,
|
|
883
|
+
toolCallCount,
|
|
884
|
+
writtenFiles,
|
|
885
|
+
toolUsageLog,
|
|
886
|
+
streamText: _streamText,
|
|
887
|
+
})).catch(() => { });
|
|
927
888
|
}
|
|
928
889
|
}
|
|
929
890
|
}, 30000);
|
|
930
891
|
|
|
931
892
|
function parseEventsFromLine(line) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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 });
|
|
936
997
|
}
|
|
937
998
|
}
|
|
938
999
|
|
|
939
1000
|
child.stdout.on('data', (data) => {
|
|
940
|
-
|
|
941
|
-
buffer
|
|
942
|
-
const lines =
|
|
943
|
-
buffer =
|
|
1001
|
+
watchdog.resetIdle();
|
|
1002
|
+
const stdoutState = splitStreamingStdoutChunk(buffer, data.toString());
|
|
1003
|
+
const lines = stdoutState.lines;
|
|
1004
|
+
buffer = stdoutState.buffer;
|
|
944
1005
|
|
|
945
1006
|
for (const line of lines) {
|
|
946
1007
|
if (!line.trim()) continue;
|
|
@@ -950,260 +1011,113 @@ function createClaudeEngine(deps) {
|
|
|
950
1011
|
}
|
|
951
1012
|
const events = parseEventsFromLine(line);
|
|
952
1013
|
for (const event of events) {
|
|
953
|
-
|
|
954
|
-
if (event.type === 'session' && event.sessionId) {
|
|
955
|
-
observedSessionId = String(event.sessionId);
|
|
956
|
-
if (typeof onSession === 'function') {
|
|
957
|
-
Promise.resolve(onSession(observedSessionId)).catch(() => { });
|
|
958
|
-
}
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
if (event.type === 'error') {
|
|
962
|
-
classifiedError = event;
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
if (event.type === 'text' && event.text) {
|
|
966
|
-
finalResult += (finalResult ? '\n\n' : '') + String(event.text);
|
|
967
|
-
_streamText = finalResult;
|
|
968
|
-
if (waitingForTool) {
|
|
969
|
-
waitingForTool = false;
|
|
970
|
-
resetIdleTimer();
|
|
971
|
-
}
|
|
972
|
-
flushStream(); // throttled stream to card
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (event.type === 'done') {
|
|
976
|
-
finalUsage = event.usage || null;
|
|
977
|
-
if (waitingForTool) {
|
|
978
|
-
waitingForTool = false;
|
|
979
|
-
resetIdleTimer();
|
|
980
|
-
}
|
|
981
|
-
// Fallback: if no text streamed yet (tool-only response), use result text from done.
|
|
982
|
-
// Do NOT use when finalResult already has content — result duplicates streamed text.
|
|
983
|
-
if (!finalResult && event.result) {
|
|
984
|
-
finalResult = String(event.result);
|
|
985
|
-
_streamText = finalResult;
|
|
986
|
-
}
|
|
987
|
-
flushStream(true); // force final text flush before process ends
|
|
988
|
-
|
|
989
|
-
// Persistent mode: finalize on result event, keep process alive for reuse
|
|
990
|
-
if (isPersistent) {
|
|
991
|
-
clearTimeout(idleTimer);
|
|
992
|
-
clearTimeout(ceilingTimer);
|
|
993
|
-
clearTimeout(sigkillTimer);
|
|
994
|
-
clearInterval(milestoneTimer);
|
|
995
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
996
|
-
// Store process back in warm pool for next turn
|
|
997
|
-
if (_warmPool && warmSessionKey && child && !child.killed && child.exitCode === null) {
|
|
998
|
-
_warmPool.storeWarm(warmSessionKey, child, { sessionId: observedSessionId, cwd });
|
|
999
|
-
}
|
|
1000
|
-
finalize({
|
|
1001
|
-
output: finalResult || '',
|
|
1002
|
-
error: null,
|
|
1003
|
-
files: writtenFiles,
|
|
1004
|
-
toolUsageLog,
|
|
1005
|
-
usage: finalUsage,
|
|
1006
|
-
sessionId: observedSessionId || '',
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
if (event.type === 'tool_result') {
|
|
1012
|
-
if (waitingForTool) {
|
|
1013
|
-
waitingForTool = false;
|
|
1014
|
-
resetIdleTimer();
|
|
1015
|
-
}
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
if (event.type !== 'tool_use') continue;
|
|
1019
|
-
|
|
1020
|
-
toolCallCount++;
|
|
1021
|
-
waitingForTool = true;
|
|
1022
|
-
resetIdleTimer();
|
|
1023
|
-
const toolName = event.toolName || 'Tool';
|
|
1024
|
-
const toolInput = event.toolInput || {};
|
|
1025
|
-
|
|
1026
|
-
const toolEntry = { tool: toolName };
|
|
1027
|
-
if (toolName === 'Skill' && toolInput.skill) toolEntry.skill = toolInput.skill;
|
|
1028
|
-
else if (toolInput.command) toolEntry.context = String(toolInput.command).slice(0, 50);
|
|
1029
|
-
else if (toolInput.file_path) toolEntry.context = path.basename(String(toolInput.file_path));
|
|
1030
|
-
if (toolUsageLog.length < 50) toolUsageLog.push(toolEntry);
|
|
1031
|
-
|
|
1032
|
-
if (toolName === 'Write' && toolInput.file_path) {
|
|
1033
|
-
const filePath = String(toolInput.file_path);
|
|
1034
|
-
if (!writtenFiles.includes(filePath)) writtenFiles.push(filePath);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const now = Date.now();
|
|
1038
|
-
if (now - lastStatusTime < STATUS_THROTTLE) continue;
|
|
1039
|
-
lastStatusTime = now;
|
|
1040
|
-
|
|
1041
|
-
const emoji = TOOL_EMOJI[toolName] || TOOL_EMOJI.default;
|
|
1042
|
-
let displayName = toolName;
|
|
1043
|
-
let displayEmoji = emoji;
|
|
1044
|
-
let context = '';
|
|
1045
|
-
|
|
1046
|
-
if (toolName === 'Skill' && toolInput.skill) {
|
|
1047
|
-
context = toolInput.skill;
|
|
1048
|
-
} else if ((toolName === 'Task' || toolName === 'Agent') && toolInput.description) {
|
|
1049
|
-
const agentType = toolInput.subagent_type ? `[${toolInput.subagent_type}] ` : '';
|
|
1050
|
-
context = (agentType + String(toolInput.description)).slice(0, 40);
|
|
1051
|
-
} else if (toolName.startsWith('mcp__')) {
|
|
1052
|
-
const parts = toolName.split('__');
|
|
1053
|
-
const server = parts[1] || 'unknown';
|
|
1054
|
-
const action = parts.slice(2).join('_') || '';
|
|
1055
|
-
if (server === 'playwright') {
|
|
1056
|
-
displayEmoji = '🌐';
|
|
1057
|
-
displayName = 'Browser';
|
|
1058
|
-
context = action.replace(/_/g, ' ');
|
|
1059
|
-
} else {
|
|
1060
|
-
displayEmoji = '🔗';
|
|
1061
|
-
displayName = `MCP:${server}`;
|
|
1062
|
-
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
1063
|
-
}
|
|
1064
|
-
} else if (toolInput.file_path) {
|
|
1065
|
-
const basename = path.basename(String(toolInput.file_path));
|
|
1066
|
-
const dotIdx = basename.lastIndexOf('.');
|
|
1067
|
-
context = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
1068
|
-
} else if (toolInput.command) {
|
|
1069
|
-
context = String(toolInput.command).slice(0, 30);
|
|
1070
|
-
if (String(toolInput.command).length > 30) context += '...';
|
|
1071
|
-
} else if (toolInput.pattern) {
|
|
1072
|
-
context = String(toolInput.pattern).slice(0, 20);
|
|
1073
|
-
} else if (toolInput.query) {
|
|
1074
|
-
context = String(toolInput.query).slice(0, 25);
|
|
1075
|
-
} else if (toolInput.url) {
|
|
1076
|
-
try { context = new URL(toolInput.url).hostname; } catch { context = 'web'; }
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const status = context
|
|
1080
|
-
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
1081
|
-
: `${displayEmoji} ${displayName}...`;
|
|
1082
|
-
if (onStatus) {
|
|
1083
|
-
// Overlay tool status on top of streamed text (if any); else show plain status
|
|
1084
|
-
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${status}` : status;
|
|
1085
|
-
onStatus(msg).catch(() => { });
|
|
1086
|
-
}
|
|
1014
|
+
applyStreamEvent(event);
|
|
1087
1015
|
}
|
|
1088
1016
|
}
|
|
1089
1017
|
});
|
|
1090
1018
|
|
|
1091
1019
|
child.stderr.on('data', (data) => {
|
|
1092
|
-
|
|
1020
|
+
watchdog.resetIdle();
|
|
1093
1021
|
const chunk = data.toString();
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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) {
|
|
1097
1030
|
log('ERROR', `[API-ERROR] ${rt.name} stderr for ${chatId}: ${chunk.slice(0, 300)}`);
|
|
1098
1031
|
}
|
|
1099
|
-
if (!classifiedError && typeof rt.classifyError === 'function') {
|
|
1100
|
-
classifiedError = rt.classifyError(chunk);
|
|
1101
|
-
}
|
|
1102
1032
|
});
|
|
1103
1033
|
|
|
1034
|
+
if (child.stdin && typeof child.stdin.on === 'function') {
|
|
1035
|
+
child.stdin.on('error', (err) => {
|
|
1036
|
+
abortForStdinFailure(err);
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1104
1040
|
child.on('close', (code) => {
|
|
1105
1041
|
log('INFO', `[TIMING:${chatId}] process-close code=${code} total=${Date.now() - _spawnAt}ms`);
|
|
1106
|
-
|
|
1107
|
-
clearTimeout(ceilingTimer);
|
|
1108
|
-
clearTimeout(sigkillTimer);
|
|
1109
|
-
clearInterval(milestoneTimer);
|
|
1042
|
+
stopStreamingLifecycle(watchdog, milestoneTimer);
|
|
1110
1043
|
|
|
1111
1044
|
// Persistent mode: if already finalized on result event, just clean up
|
|
1112
1045
|
if (isPersistent && settled) {
|
|
1113
|
-
|
|
1046
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
1114
1047
|
// Process died after we returned result — remove from warm pool
|
|
1115
1048
|
if (_warmPool && warmSessionKey) _warmPool.releaseWarm(warmSessionKey);
|
|
1116
1049
|
return;
|
|
1117
1050
|
}
|
|
1118
1051
|
|
|
1119
|
-
|
|
1120
|
-
const events = parseEventsFromLine(buffer.trim());
|
|
1121
|
-
for (const event of events) {
|
|
1122
|
-
if (event.type === 'text' && event.text) finalResult = String(event.text);
|
|
1123
|
-
if (event.type === 'done') finalUsage = event.usage || null;
|
|
1124
|
-
if (event.type === 'session' && event.sessionId) observedSessionId = String(event.sessionId);
|
|
1125
|
-
if (event.type === 'error') classifiedError = event;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1052
|
+
absorbBufferedEvents();
|
|
1128
1053
|
|
|
1129
1054
|
const proc = chatId ? activeProcesses.get(chatId) : null;
|
|
1130
1055
|
const wasAborted = proc && proc.aborted;
|
|
1131
1056
|
const abortReason = proc && proc.abortReason ? String(proc.abortReason) : '';
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
if (killed) {
|
|
1152
|
-
const elapsed = Math.round((Date.now() - startTime) / 60000);
|
|
1153
|
-
const idleMin = Math.max(1, Math.round(IDLE_TIMEOUT_MS / 60000));
|
|
1154
|
-
const reason = killedReason === 'ceiling'
|
|
1155
|
-
? `⏱ 已运行 ${elapsed} 分钟,达到上限(${Math.round(HARD_CEILING_MS / 60000)} 分钟)`
|
|
1156
|
-
: `⏱ 已 ${idleMin} 分钟无输出,判定卡死(共运行 ${elapsed} 分钟)`;
|
|
1157
|
-
finalize({ output: finalResult || null, error: reason, timedOut: true, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (code !== 0) {
|
|
1161
|
-
const engineErr = classifiedError && classifiedError.message
|
|
1162
|
-
? classifiedError.message
|
|
1163
|
-
: (stderr || `Exit code ${code}`);
|
|
1164
|
-
finalize({ output: finalResult || null, error: engineErr, errorCode: classifiedError ? classifiedError.code : undefined, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
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
|
+
}));
|
|
1168
1075
|
});
|
|
1169
1076
|
|
|
1170
1077
|
child.on('error', (err) => {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
clearTimeout(sigkillTimer);
|
|
1174
|
-
clearInterval(milestoneTimer);
|
|
1175
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
1078
|
+
stopStreamingLifecycle(watchdog, milestoneTimer);
|
|
1079
|
+
clearActiveChildProcess(activeProcesses, saveActivePids, chatId);
|
|
1176
1080
|
finalize({ output: null, error: formatEngineSpawnError(err, rt), files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
1177
1081
|
});
|
|
1178
1082
|
|
|
1179
1083
|
try {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1084
|
+
writeStreamingChildInput({
|
|
1085
|
+
child,
|
|
1086
|
+
input,
|
|
1087
|
+
isPersistent,
|
|
1088
|
+
warmPool: _warmPool,
|
|
1089
|
+
observedSessionId,
|
|
1090
|
+
});
|
|
1187
1091
|
} catch (e) {
|
|
1188
|
-
|
|
1189
|
-
clearTimeout(ceilingTimer);
|
|
1190
|
-
clearTimeout(sigkillTimer);
|
|
1191
|
-
clearInterval(milestoneTimer);
|
|
1192
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
1193
|
-
try { child.stdin.destroy(); } catch { /* ignore */ }
|
|
1194
|
-
try {
|
|
1195
|
-
const sig = rt.killSignal || 'SIGTERM';
|
|
1196
|
-
process.kill(-child.pid, sig);
|
|
1197
|
-
} catch {
|
|
1198
|
-
try { child.kill(rt.killSignal || 'SIGTERM'); } catch { /* ignore */ }
|
|
1199
|
-
}
|
|
1200
|
-
finalize({ output: null, error: e.message, files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
1092
|
+
abortForStdinFailure(e);
|
|
1201
1093
|
}
|
|
1202
1094
|
});
|
|
1203
1095
|
}
|
|
1204
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
|
+
|
|
1205
1118
|
// Track outbound message_id → session for reply-based session restoration.
|
|
1206
|
-
//
|
|
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.
|
|
1207
1121
|
function trackMsgSession(messageId, session, agentKey, options = {}) {
|
|
1208
1122
|
if (!messageId || !session) return;
|
|
1209
1123
|
const forceRouteOnly = !!(options && options.routeOnly);
|
|
@@ -1219,11 +1133,9 @@ function createClaudeEngine(deps) {
|
|
|
1219
1133
|
...(session.sandboxMode ? { sandboxMode: session.sandboxMode } : {}),
|
|
1220
1134
|
...(session.approvalPolicy ? { approvalPolicy: session.approvalPolicy } : {}),
|
|
1221
1135
|
...(session.permissionMode ? { permissionMode: session.permissionMode } : {}),
|
|
1136
|
+
touchedAt: Date.now(),
|
|
1222
1137
|
};
|
|
1223
|
-
|
|
1224
|
-
if (keys.length > 200) {
|
|
1225
|
-
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
1226
|
-
}
|
|
1138
|
+
st.msg_sessions = pruneMsgSessionMappings(st.msg_sessions);
|
|
1227
1139
|
saveState(st);
|
|
1228
1140
|
}
|
|
1229
1141
|
|
|
@@ -1236,23 +1148,19 @@ function createClaudeEngine(deps) {
|
|
|
1236
1148
|
* Reset active provider back to anthropic/opus and reload config.
|
|
1237
1149
|
* Returns the freshly loaded config so callers can reassign their local variable.
|
|
1238
1150
|
*/
|
|
1239
|
-
function fallbackToDefaultProvider(reason
|
|
1151
|
+
function fallbackToDefaultProvider(reason) {
|
|
1240
1152
|
log('WARN', `Falling back to anthropic/opus — reason: ${reason}`);
|
|
1241
1153
|
if (providerMod && providerMod.getActiveName() !== 'anthropic') {
|
|
1242
1154
|
providerMod.setActive('anthropic');
|
|
1243
1155
|
}
|
|
1244
1156
|
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
1245
1157
|
if (!cfg.daemon) cfg.daemon = {};
|
|
1246
|
-
|
|
1247
|
-
cfg.daemon.models.claude = 'opus';
|
|
1248
|
-
if (boundProjectKey && cfg.projects && cfg.projects[boundProjectKey]) {
|
|
1249
|
-
cfg.projects[boundProjectKey].model = 'opus';
|
|
1250
|
-
}
|
|
1158
|
+
cfg.daemon.model = 'opus';
|
|
1251
1159
|
writeConfigSafe(cfg);
|
|
1252
1160
|
return loadConfig();
|
|
1253
1161
|
}
|
|
1254
1162
|
|
|
1255
|
-
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null
|
|
1163
|
+
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null) {
|
|
1256
1164
|
const _t0 = Date.now();
|
|
1257
1165
|
log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
|
|
1258
1166
|
|
|
@@ -1262,17 +1170,15 @@ function createClaudeEngine(deps) {
|
|
|
1262
1170
|
const _existing = activeProcesses.get(chatId);
|
|
1263
1171
|
if (_existing && _existing.child && !_existing.aborted) {
|
|
1264
1172
|
log('WARN', `askClaude: overwriting active process for ${chatId} — aborting previous`);
|
|
1265
|
-
|
|
1173
|
+
terminateChildProcess(_existing.child, 'SIGTERM', { useProcessGroup: process.platform !== 'win32' });
|
|
1266
1174
|
}
|
|
1267
1175
|
activeProcesses.set(chatId, {
|
|
1268
|
-
chatId,
|
|
1269
1176
|
child: null, // sentinel: no process yet
|
|
1270
1177
|
aborted: false,
|
|
1271
1178
|
abortReason: null,
|
|
1272
1179
|
startedAt: _t0,
|
|
1273
1180
|
engine: 'pending',
|
|
1274
1181
|
killSignal: 'SIGTERM',
|
|
1275
|
-
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
1276
1182
|
});
|
|
1277
1183
|
|
|
1278
1184
|
// Track interaction time for idle/sleep detection
|
|
@@ -1296,7 +1202,7 @@ function createClaudeEngine(deps) {
|
|
|
1296
1202
|
...(config.feishu ? config.feishu.chat_agent_map || {} : {}),
|
|
1297
1203
|
...(config.imessage ? config.imessage.chat_agent_map || {} : {}),
|
|
1298
1204
|
};
|
|
1299
|
-
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
1205
|
+
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || _ackAgentMap[rawChatId(_ackChatIdStr)] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
1300
1206
|
const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
|
|
1301
1207
|
// _ackCardHeader: non-null for bound projects with a name; passed to editMessage to preserve header on streaming edits
|
|
1302
1208
|
let _ackCardHeader = (_ackBoundProj && _ackBoundProj.name)
|
|
@@ -1349,7 +1255,7 @@ function createClaudeEngine(deps) {
|
|
|
1349
1255
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1350
1256
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1351
1257
|
};
|
|
1352
|
-
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
1258
|
+
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || _strictAgentMap[rawChatId(String(chatId))] || projectKeyFromVirtualChatId(String(chatId)));
|
|
1353
1259
|
const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
|
|
1354
1260
|
if (agentMatch) {
|
|
1355
1261
|
const { key, proj, rest } = agentMatch;
|
|
@@ -1378,7 +1284,7 @@ function createClaudeEngine(deps) {
|
|
|
1378
1284
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1379
1285
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1380
1286
|
};
|
|
1381
|
-
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
1287
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || chatAgentMap[rawChatId(chatIdStr)] || projectKeyFromVirtualChatId(chatIdStr);
|
|
1382
1288
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
1383
1289
|
const daemonCfg = (config && config.daemon) || {};
|
|
1384
1290
|
// Keep real group chats on their own session key.
|
|
@@ -1397,7 +1303,6 @@ function createClaudeEngine(deps) {
|
|
|
1397
1303
|
(boundProject && boundProject.engine) || getDefaultEngine()
|
|
1398
1304
|
);
|
|
1399
1305
|
const runtime = getEngineRuntime(engineName);
|
|
1400
|
-
const executionTimeouts = resolveEngineTimeouts(engineName, { reactive: !!(meta && meta.reactive) });
|
|
1401
1306
|
const requestedCodexPermissionProfile = engineName === 'codex'
|
|
1402
1307
|
? getCodexPermissionProfile(readOnly, daemonCfg)
|
|
1403
1308
|
: null;
|
|
@@ -1576,7 +1481,7 @@ function createClaudeEngine(deps) {
|
|
|
1576
1481
|
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1577
1482
|
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1578
1483
|
};
|
|
1579
|
-
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1484
|
+
const projectKey = _agentMap0[_cid0] || _agentMap0[rawChatId(_cid0)] || projectKeyFromVirtualChatId(_cid0);
|
|
1580
1485
|
try {
|
|
1581
1486
|
const memory = require('./memory');
|
|
1582
1487
|
|
|
@@ -1759,6 +1664,25 @@ ${mentorRadarHint}
|
|
|
1759
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).]`;
|
|
1760
1665
|
}
|
|
1761
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
|
+
|
|
1762
1686
|
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1763
1687
|
let mentorHint = '';
|
|
1764
1688
|
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
@@ -1830,7 +1754,7 @@ ${mentorRadarHint}
|
|
|
1830
1754
|
// (varies per prompt), so include it even on warm reuse.
|
|
1831
1755
|
const fullPrompt = _warmEntry
|
|
1832
1756
|
? routedPrompt + intentHint
|
|
1833
|
-
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + memoryHint + mentorHint + langGuard;
|
|
1757
|
+
: routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
1834
1758
|
if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
|
|
1835
1759
|
const actualPermissionProfile = getActualCodexPermissionProfile(session);
|
|
1836
1760
|
if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
|
|
@@ -2028,8 +1952,6 @@ ${mentorRadarHint}
|
|
|
2028
1952
|
persistent: runtime.name === 'claude' && !!warmPool,
|
|
2029
1953
|
warmPool,
|
|
2030
1954
|
warmSessionKey: _warmSessionKey,
|
|
2031
|
-
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2032
|
-
timeouts: executionTimeouts,
|
|
2033
1955
|
},
|
|
2034
1956
|
));
|
|
2035
1957
|
|
|
@@ -2094,10 +2016,6 @@ ${mentorRadarHint}
|
|
|
2094
2016
|
normalizeSenderId(senderId),
|
|
2095
2017
|
runtime,
|
|
2096
2018
|
onSession,
|
|
2097
|
-
{
|
|
2098
|
-
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2099
|
-
timeouts: executionTimeouts,
|
|
2100
|
-
},
|
|
2101
2019
|
));
|
|
2102
2020
|
if (sessionId) await onSession(sessionId);
|
|
2103
2021
|
observedRuntimeProfile = getActualCodexPermissionProfile(sessionId ? { id: sessionId } : session);
|
|
@@ -2127,10 +2045,10 @@ ${mentorRadarHint}
|
|
|
2127
2045
|
markCodexResumeRetried(chatId, resumeFailure.kind);
|
|
2128
2046
|
log(
|
|
2129
2047
|
'WARN',
|
|
2130
|
-
`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)}`
|
|
2131
2049
|
);
|
|
2132
2050
|
await bot.sendMessage(chatId, resumeFailure.userMessage).catch(() => { });
|
|
2133
|
-
if (resumeFailure.kind !== 'interrupted') {
|
|
2051
|
+
if (resumeFailure.kind !== 'interrupted' && resumeFailure.kind !== 'transport') {
|
|
2134
2052
|
session = createSession(
|
|
2135
2053
|
sessionChatId,
|
|
2136
2054
|
effectiveCwd,
|
|
@@ -2167,10 +2085,6 @@ ${mentorRadarHint}
|
|
|
2167
2085
|
normalizeSenderId(senderId),
|
|
2168
2086
|
runtime,
|
|
2169
2087
|
onSession,
|
|
2170
|
-
{
|
|
2171
|
-
reactiveProjectKey: String(meta && meta.reactiveProjectKey || '').trim(),
|
|
2172
|
-
timeouts: executionTimeouts,
|
|
2173
|
-
},
|
|
2174
2088
|
));
|
|
2175
2089
|
if (sessionId) await onSession(sessionId);
|
|
2176
2090
|
}
|
|
@@ -2268,7 +2182,7 @@ ${mentorRadarHint}
|
|
|
2268
2182
|
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
2269
2183
|
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
2270
2184
|
try {
|
|
2271
|
-
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}
|
|
2185
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
2272
2186
|
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
2273
2187
|
} catch (fbErr) {
|
|
2274
2188
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
@@ -2449,13 +2363,26 @@ ${mentorRadarHint}
|
|
|
2449
2363
|
return { ok: false, error: errMsg, errorCode };
|
|
2450
2364
|
}
|
|
2451
2365
|
|
|
2452
|
-
// 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)
|
|
2453
2367
|
const _isThinkingSignatureError = isClaudeThinkingSignatureError(errMsg);
|
|
2454
2368
|
const _isSessionResumeFail = errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use') || _isThinkingSignatureError;
|
|
2455
2369
|
if (runtime.name === 'claude' && _isSessionResumeFail) {
|
|
2456
2370
|
const _reason = errMsg.includes('already in use') ? 'locked' : _isThinkingSignatureError ? 'thinking-signature-invalid' : 'not found';
|
|
2457
|
-
|
|
2458
|
-
|
|
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
|
+
}
|
|
2459
2386
|
|
|
2460
2387
|
const retryArgs = runtime.buildArgs({
|
|
2461
2388
|
model,
|
|
@@ -2499,7 +2426,7 @@ ${mentorRadarHint}
|
|
|
2499
2426
|
const builtinModelValues = (ENGINE_MODEL_CONFIG.claude.options || []).map(o => typeof o === 'string' ? o : o.value);
|
|
2500
2427
|
if ((activeProv !== 'anthropic' || !builtinModelValues.includes(model)) && !errMsg.includes('Stopped by user')) {
|
|
2501
2428
|
try {
|
|
2502
|
-
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}
|
|
2429
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
2503
2430
|
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
2504
2431
|
} catch (fallbackErr) {
|
|
2505
2432
|
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
@@ -2537,6 +2464,8 @@ ${mentorRadarHint}
|
|
|
2537
2464
|
_private: {
|
|
2538
2465
|
patchSessionSerialized,
|
|
2539
2466
|
shouldRetryCodexResumeFallback,
|
|
2467
|
+
resolveStreamingTimeouts,
|
|
2468
|
+
formatTimeoutWindowLabel,
|
|
2540
2469
|
formatEngineSpawnError,
|
|
2541
2470
|
adaptDaemonHintForEngine,
|
|
2542
2471
|
getSessionChatId,
|