metame-cli 1.4.33 → 1.5.0
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/README.md +187 -48
- package/index.js +148 -9
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +254 -9
- package/scripts/daemon-agent-commands.js +64 -6
- package/scripts/daemon-agent-tools.js +26 -5
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-claude-engine.js +704 -268
- package/scripts/daemon-command-router.js +24 -8
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +275 -0
- package/scripts/daemon-exec-commands.js +10 -4
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-runtime-lifecycle.js +2 -1
- package/scripts/daemon-session-commands.js +52 -4
- package/scripts/daemon-session-store.js +2 -1
- package/scripts/daemon-task-scheduler.js +87 -28
- package/scripts/daemon-user-acl.js +26 -9
- package/scripts/daemon.js +81 -17
- package/scripts/distill.js +323 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +119 -0
- package/scripts/docs/pointer-map.md +88 -0
- package/scripts/feishu-adapter.js +6 -1
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +100 -5
- package/scripts/memory-nightly-reflect.js +196 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +2 -0
- package/scripts/providers.js +169 -21
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { classifyChatUsage } = require('./usage-classifier');
|
|
4
|
+
const { deriveProjectInfo } = require('./utils');
|
|
5
|
+
const { createEngineRuntimeFactory, normalizeEngineName } = require('./daemon-engine-runtime');
|
|
4
6
|
|
|
5
7
|
function createClaudeEngine(deps) {
|
|
6
8
|
const {
|
|
@@ -40,16 +42,92 @@ function createClaudeEngine(deps) {
|
|
|
40
42
|
touchInteraction,
|
|
41
43
|
statusThrottleMs = 3000,
|
|
42
44
|
fallbackThrottleMs = 8000,
|
|
45
|
+
getEngineRuntime: injectedGetEngineRuntime,
|
|
46
|
+
getDefaultEngine: _getDefaultEngine,
|
|
43
47
|
} = deps;
|
|
48
|
+
function getDefaultEngine() {
|
|
49
|
+
return (typeof _getDefaultEngine === 'function') ? _getDefaultEngine() : 'claude';
|
|
50
|
+
}
|
|
51
|
+
let mentorEngine = null;
|
|
52
|
+
try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
|
|
53
|
+
let sessionAnalytics = null;
|
|
54
|
+
try { sessionAnalytics = require('./session-analytics'); } catch { /* optional */ }
|
|
55
|
+
|
|
56
|
+
const getEngineRuntime = typeof injectedGetEngineRuntime === 'function'
|
|
57
|
+
? injectedGetEngineRuntime
|
|
58
|
+
: createEngineRuntimeFactory({ fs, path, HOME, CLAUDE_BIN, getActiveProviderEnv });
|
|
44
59
|
|
|
45
60
|
// On Windows, .cmd files need shell to spawn; use COMSPEC to avoid conda PATH issues
|
|
46
61
|
function spawn(cmd, args, options) {
|
|
47
|
-
|
|
62
|
+
const lowerCmd = String(cmd || '').toLowerCase();
|
|
63
|
+
if (process.platform === 'win32' && (cmd === CLAUDE_BIN || lowerCmd.endsWith('\\claude.cmd') || lowerCmd.endsWith('\\codex.cmd'))) {
|
|
48
64
|
return _spawn(cmd, args, { ...options, shell: process.env.COMSPEC || true });
|
|
49
65
|
}
|
|
50
66
|
return _spawn(cmd, args, options);
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
let _sessionPatchQueue = Promise.resolve();
|
|
70
|
+
function patchSessionSerialized(chatId, patchFn) {
|
|
71
|
+
_sessionPatchQueue = _sessionPatchQueue.then(() => {
|
|
72
|
+
const state = loadState();
|
|
73
|
+
if (!state.sessions) state.sessions = {};
|
|
74
|
+
const cur = state.sessions[chatId] || {};
|
|
75
|
+
const next = typeof patchFn === 'function' ? patchFn(cur) : cur;
|
|
76
|
+
state.sessions[chatId] = next && typeof next === 'object' ? next : cur;
|
|
77
|
+
saveState(state);
|
|
78
|
+
}).catch((e) => {
|
|
79
|
+
log('WARN', `patchSessionSerialized failed: ${e.message}`);
|
|
80
|
+
});
|
|
81
|
+
return _sessionPatchQueue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const CODEX_RESUME_RETRY_WINDOW_MS = 10 * 60 * 1000;
|
|
85
|
+
const _codexResumeRetryTs = new Map(); // chatId -> last retry ts
|
|
86
|
+
|
|
87
|
+
function canRetryCodexResume(chatId) {
|
|
88
|
+
const key = String(chatId || '');
|
|
89
|
+
if (!key) return false;
|
|
90
|
+
const last = Number(_codexResumeRetryTs.get(key) || 0);
|
|
91
|
+
if (!last) return true;
|
|
92
|
+
return (Date.now() - last) > CODEX_RESUME_RETRY_WINDOW_MS;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function markCodexResumeRetried(chatId) {
|
|
96
|
+
const key = String(chatId || '');
|
|
97
|
+
if (!key) return;
|
|
98
|
+
_codexResumeRetryTs.set(key, Date.now());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldRetryCodexResumeFallback({ runtimeName, wasResumeAttempt, output, error, errorCode, canRetry }) {
|
|
102
|
+
return runtimeName === 'codex'
|
|
103
|
+
&& !!wasResumeAttempt
|
|
104
|
+
&& !!error
|
|
105
|
+
&& (!output || !!errorCode)
|
|
106
|
+
&& !!canRetry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatEngineSpawnError(err, runtime) {
|
|
110
|
+
if (!err) return 'Unknown spawn error';
|
|
111
|
+
const rt = runtime || { name: getDefaultEngine() };
|
|
112
|
+
if (err.code === 'ENOENT') {
|
|
113
|
+
if (rt.name === 'codex') {
|
|
114
|
+
return 'Codex CLI 未安装。请先运行: npm install -g @openai/codex';
|
|
115
|
+
}
|
|
116
|
+
return 'Claude CLI 未安装或不在 PATH。请先确认 `claude` 可执行。';
|
|
117
|
+
}
|
|
118
|
+
return err.message || String(err);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function adaptDaemonHintForEngine(daemonHint, engineName) {
|
|
122
|
+
if (normalizeEngineName(engineName) === 'claude') return daemonHint;
|
|
123
|
+
let out = String(daemonHint || '');
|
|
124
|
+
// Keep this replacement conservative: only unwrap the known outer wrapper.
|
|
125
|
+
out = out.replace('[System hints - DO NOT mention these to user:', 'System hints (internal, do not mention to user):');
|
|
126
|
+
// The current daemonHint template ends with a single trailing `]`.
|
|
127
|
+
out = out.replace(/\]\s*$/, '');
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
53
131
|
const SESSION_CWD_VALIDATION_TTL_MS = 30 * 1000;
|
|
54
132
|
const _sessionCwdValidationCache = new Map(); // key: `${sessionId}@@${cwd}` -> { inCwd, ts }
|
|
55
133
|
|
|
@@ -192,6 +270,82 @@ function createClaudeEngine(deps) {
|
|
|
192
270
|
return null;
|
|
193
271
|
}
|
|
194
272
|
|
|
273
|
+
function resolveMentorMode(cfg = {}) {
|
|
274
|
+
const mode = String(cfg.mode || '').trim().toLowerCase();
|
|
275
|
+
if (mode === 'gentle' || mode === 'active' || mode === 'intense') return mode;
|
|
276
|
+
const level = Number(cfg.friction_level);
|
|
277
|
+
if (Number.isFinite(level)) {
|
|
278
|
+
if (level >= 8) return 'intense';
|
|
279
|
+
if (level >= 4) return 'active';
|
|
280
|
+
}
|
|
281
|
+
return 'gentle';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function extractUserText(content) {
|
|
285
|
+
if (typeof content === 'string') return content;
|
|
286
|
+
if (!Array.isArray(content)) return '';
|
|
287
|
+
for (const item of content) {
|
|
288
|
+
if (item && item.type === 'text' && item.text) return item.text;
|
|
289
|
+
}
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function collectRecentSessionSignals(sessionId, limit = 6) {
|
|
294
|
+
const out = { recentMessages: [], sessionStartTime: null };
|
|
295
|
+
if (!sessionId || typeof findSessionFile !== 'function') return out;
|
|
296
|
+
const file = findSessionFile(sessionId);
|
|
297
|
+
if (!file || !fs.existsSync(file)) return out;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
301
|
+
const lines = raw.split('\n').filter(Boolean).slice(-800);
|
|
302
|
+
let current = null;
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
let entry;
|
|
305
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
306
|
+
if (!out.sessionStartTime && entry.timestamp) out.sessionStartTime = entry.timestamp;
|
|
307
|
+
|
|
308
|
+
if (entry.type === 'user' && entry.message) {
|
|
309
|
+
if (current) out.recentMessages.push(current);
|
|
310
|
+
current = {
|
|
311
|
+
text: extractUserText(entry.message.content),
|
|
312
|
+
tool_calls: 0,
|
|
313
|
+
};
|
|
314
|
+
} else if (entry.type === 'assistant' && current && entry.message && Array.isArray(entry.message.content)) {
|
|
315
|
+
for (const item of entry.message.content) {
|
|
316
|
+
if (item && item.type === 'tool_use') current.tool_calls++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (current) out.recentMessages.push(current);
|
|
321
|
+
if (out.recentMessages.length > limit) {
|
|
322
|
+
out.recentMessages = out.recentMessages.slice(-limit);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function countCodeLines(output) {
|
|
331
|
+
const text = String(output || '');
|
|
332
|
+
if (!text.trim()) return 0;
|
|
333
|
+
const lines = text.split('\n');
|
|
334
|
+
let inFence = false;
|
|
335
|
+
let count = 0;
|
|
336
|
+
let sawFence = false;
|
|
337
|
+
for (const line of lines) {
|
|
338
|
+
if (/^\s*```/.test(line)) {
|
|
339
|
+
sawFence = true;
|
|
340
|
+
inFence = !inFence;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (inFence && line.trim()) count++;
|
|
344
|
+
}
|
|
345
|
+
if (!sawFence) return 0;
|
|
346
|
+
return count;
|
|
347
|
+
}
|
|
348
|
+
|
|
195
349
|
function isMacAutomationIntent(prompt) {
|
|
196
350
|
const text = String(prompt || '').trim();
|
|
197
351
|
if (!text) return false;
|
|
@@ -243,21 +397,24 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
243
397
|
}
|
|
244
398
|
|
|
245
399
|
/**
|
|
246
|
-
* Spawn
|
|
400
|
+
* Spawn Claude as async child process (non-blocking).
|
|
401
|
+
* Intentionally Claude-only: used by naming/fallback helper paths that
|
|
402
|
+
* should not depend on project runtime adapter selection.
|
|
247
403
|
* Returns { output, error } after process exits.
|
|
248
404
|
*/
|
|
249
405
|
function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000, metameProject = '') {
|
|
250
406
|
return new Promise((resolve) => {
|
|
407
|
+
const env = {
|
|
408
|
+
...process.env,
|
|
409
|
+
...getActiveProviderEnv(),
|
|
410
|
+
METAME_INTERNAL_PROMPT: '1',
|
|
411
|
+
METAME_PROJECT: metameProject || '',
|
|
412
|
+
};
|
|
413
|
+
delete env.CLAUDECODE;
|
|
251
414
|
const child = spawn(CLAUDE_BIN, args, {
|
|
252
415
|
cwd,
|
|
253
416
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
254
|
-
env
|
|
255
|
-
...process.env,
|
|
256
|
-
...getActiveProviderEnv(),
|
|
257
|
-
CLAUDECODE: undefined,
|
|
258
|
-
METAME_INTERNAL_PROMPT: '1',
|
|
259
|
-
METAME_PROJECT: metameProject || ''
|
|
260
|
-
},
|
|
417
|
+
env,
|
|
261
418
|
});
|
|
262
419
|
|
|
263
420
|
let stdout = '';
|
|
@@ -288,7 +445,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
288
445
|
|
|
289
446
|
child.on('error', (err) => {
|
|
290
447
|
clearTimeout(timer);
|
|
291
|
-
resolve({ output: null, error: err
|
|
448
|
+
resolve({ output: null, error: formatEngineSpawnError(err, { name: getDefaultEngine() }) });
|
|
292
449
|
});
|
|
293
450
|
|
|
294
451
|
// Write input and close stdin
|
|
@@ -317,55 +474,77 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
317
474
|
};
|
|
318
475
|
|
|
319
476
|
/**
|
|
320
|
-
* Spawn
|
|
321
|
-
*
|
|
322
|
-
* Returns { output, error } after process exits.
|
|
477
|
+
* Spawn engine with streaming output. Parser comes from runtime adapter.
|
|
478
|
+
* Returns { output, error, files, toolUsageLog, usage, sessionId }.
|
|
323
479
|
*/
|
|
324
|
-
function spawnClaudeStreaming(
|
|
480
|
+
function spawnClaudeStreaming(
|
|
481
|
+
args,
|
|
482
|
+
input,
|
|
483
|
+
cwd,
|
|
484
|
+
onStatus,
|
|
485
|
+
timeoutMs = 600000,
|
|
486
|
+
chatId = null,
|
|
487
|
+
metameProject = '',
|
|
488
|
+
runtime = null,
|
|
489
|
+
onSession = null,
|
|
490
|
+
) {
|
|
325
491
|
return new Promise((resolve) => {
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
492
|
+
let settled = false;
|
|
493
|
+
const finalize = (payload) => {
|
|
494
|
+
if (settled) return;
|
|
495
|
+
settled = true;
|
|
496
|
+
resolve(payload);
|
|
497
|
+
};
|
|
498
|
+
const rt = runtime || getEngineRuntime(getDefaultEngine());
|
|
499
|
+
const streamArgs = rt.name === 'claude'
|
|
500
|
+
? [...args, '--output-format', 'stream-json', '--verbose']
|
|
501
|
+
: args;
|
|
502
|
+
const child = spawn(rt.binary, streamArgs, {
|
|
330
503
|
cwd,
|
|
331
504
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
332
|
-
detached: process.platform !== 'win32',
|
|
333
|
-
env: {
|
|
334
|
-
...process.env,
|
|
335
|
-
...getActiveProviderEnv(),
|
|
336
|
-
CLAUDECODE: undefined,
|
|
337
|
-
METAME_PROJECT: metameProject || ''
|
|
338
|
-
},
|
|
505
|
+
detached: process.platform !== 'win32',
|
|
506
|
+
env: rt.buildEnv({ metameProject }),
|
|
339
507
|
});
|
|
340
508
|
|
|
341
|
-
// Track active process for /stop
|
|
342
509
|
if (chatId) {
|
|
343
|
-
activeProcesses.set(chatId, {
|
|
344
|
-
|
|
510
|
+
activeProcesses.set(chatId, {
|
|
511
|
+
child,
|
|
512
|
+
aborted: false,
|
|
513
|
+
startedAt: Date.now(),
|
|
514
|
+
engine: rt.name,
|
|
515
|
+
killSignal: rt.killSignal || 'SIGTERM',
|
|
516
|
+
});
|
|
517
|
+
saveActivePids();
|
|
345
518
|
}
|
|
346
519
|
|
|
347
520
|
let buffer = '';
|
|
348
521
|
let stderr = '';
|
|
349
522
|
let killed = false;
|
|
350
|
-
let killedReason = 'idle';
|
|
523
|
+
let killedReason = 'idle';
|
|
351
524
|
let finalResult = '';
|
|
525
|
+
let finalUsage = null;
|
|
526
|
+
let observedSessionId = '';
|
|
527
|
+
let classifiedError = null;
|
|
352
528
|
let lastStatusTime = 0;
|
|
353
529
|
const STATUS_THROTTLE = statusThrottleMs;
|
|
354
|
-
const writtenFiles = [];
|
|
355
|
-
const toolUsageLog = [];
|
|
530
|
+
const writtenFiles = [];
|
|
531
|
+
const toolUsageLog = [];
|
|
356
532
|
|
|
357
|
-
|
|
358
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
359
|
-
const
|
|
533
|
+
const engineTimeouts = rt.timeouts || {};
|
|
534
|
+
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs || (5 * 60 * 1000);
|
|
535
|
+
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs || (25 * 60 * 1000);
|
|
536
|
+
const HARD_CEILING_MS = engineTimeouts.ceilingMs || (60 * 60 * 1000);
|
|
360
537
|
const startTime = Date.now();
|
|
538
|
+
let waitingForTool = false;
|
|
361
539
|
|
|
362
540
|
let sigkillTimer = null;
|
|
363
541
|
function killChild(reason) {
|
|
364
542
|
if (killed) return;
|
|
365
543
|
killed = true;
|
|
366
544
|
killedReason = reason;
|
|
367
|
-
log('WARN', `
|
|
368
|
-
|
|
545
|
+
log('WARN', `[${rt.name}] ${reason} timeout for chatId ${chatId} — killing process group`);
|
|
546
|
+
const sig = rt.killSignal || 'SIGTERM';
|
|
547
|
+
try { process.kill(-child.pid, sig); } catch { child.kill(sig); }
|
|
369
548
|
sigkillTimer = setTimeout(() => {
|
|
370
549
|
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { } }
|
|
371
550
|
}, 5000);
|
|
@@ -376,10 +555,10 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
376
555
|
|
|
377
556
|
function resetIdleTimer() {
|
|
378
557
|
clearTimeout(idleTimer);
|
|
379
|
-
|
|
558
|
+
const timeout = waitingForTool ? TOOL_EXEC_TIMEOUT_MS : IDLE_TIMEOUT_MS;
|
|
559
|
+
idleTimer = setTimeout(() => killChild('idle'), timeout);
|
|
380
560
|
}
|
|
381
561
|
|
|
382
|
-
// ── 进度里程碑:2min 首报,之后每 5min 一次 ──
|
|
383
562
|
let toolCallCount = 0;
|
|
384
563
|
let lastMilestoneMin = 0;
|
|
385
564
|
const milestoneTimer = setInterval(() => {
|
|
@@ -400,125 +579,134 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
400
579
|
}
|
|
401
580
|
}, 30000);
|
|
402
581
|
|
|
582
|
+
function parseEventsFromLine(line) {
|
|
583
|
+
try {
|
|
584
|
+
return rt.parseStreamEvent(line) || [];
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
403
590
|
child.stdout.on('data', (data) => {
|
|
404
591
|
resetIdleTimer();
|
|
405
592
|
buffer += data.toString();
|
|
406
|
-
|
|
407
|
-
// Process complete JSON lines
|
|
408
593
|
const lines = buffer.split('\n');
|
|
409
|
-
buffer = lines.pop() || '';
|
|
594
|
+
buffer = lines.pop() || '';
|
|
410
595
|
|
|
411
596
|
for (const line of lines) {
|
|
412
597
|
if (!line.trim()) continue;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
finalResult = textBlocks.map(b => b.text).join('\n');
|
|
598
|
+
const events = parseEventsFromLine(line);
|
|
599
|
+
for (const event of events) {
|
|
600
|
+
if (!event || !event.type) continue;
|
|
601
|
+
if (event.type === 'session' && event.sessionId) {
|
|
602
|
+
observedSessionId = String(event.sessionId);
|
|
603
|
+
if (typeof onSession === 'function') {
|
|
604
|
+
Promise.resolve(onSession(observedSessionId)).catch(() => { });
|
|
421
605
|
}
|
|
606
|
+
continue;
|
|
422
607
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (now - lastStatusTime >= STATUS_THROTTLE) {
|
|
448
|
-
lastStatusTime = now;
|
|
449
|
-
const emoji = TOOL_EMOJI[toolName] || TOOL_EMOJI.default;
|
|
450
|
-
|
|
451
|
-
// Resolve display name and context for MCP/Skill/Task tools
|
|
452
|
-
let displayName = toolName;
|
|
453
|
-
let displayEmoji = emoji;
|
|
454
|
-
let context = '';
|
|
455
|
-
|
|
456
|
-
if (toolName === 'Skill' && block.input?.skill) {
|
|
457
|
-
// Skill invocation: show skill name
|
|
458
|
-
context = block.input.skill;
|
|
459
|
-
} else if (toolName === 'Task' && block.input?.description) {
|
|
460
|
-
// Agent task: show description
|
|
461
|
-
context = block.input.description.slice(0, 30);
|
|
462
|
-
} else if (toolName.startsWith('mcp__')) {
|
|
463
|
-
// MCP tool: mcp__server__action → "MCP server: action"
|
|
464
|
-
const parts = toolName.split('__');
|
|
465
|
-
const server = parts[1] || 'unknown';
|
|
466
|
-
const action = parts.slice(2).join('_') || '';
|
|
467
|
-
if (server === 'playwright') {
|
|
468
|
-
displayEmoji = '🌐';
|
|
469
|
-
displayName = 'Browser';
|
|
470
|
-
context = action.replace(/_/g, ' ');
|
|
471
|
-
} else {
|
|
472
|
-
displayEmoji = '🔗';
|
|
473
|
-
displayName = `MCP:${server}`;
|
|
474
|
-
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
475
|
-
}
|
|
476
|
-
} else if (block.input) {
|
|
477
|
-
// Standard tools: extract brief context
|
|
478
|
-
if (block.input.file_path) {
|
|
479
|
-
// Insert zero-width space before extension to prevent link parsing
|
|
480
|
-
const basename = path.basename(block.input.file_path);
|
|
481
|
-
const dotIdx = basename.lastIndexOf('.');
|
|
482
|
-
context = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
483
|
-
} else if (block.input.command) {
|
|
484
|
-
context = block.input.command.slice(0, 30);
|
|
485
|
-
if (block.input.command.length > 30) context += '...';
|
|
486
|
-
} else if (block.input.pattern) {
|
|
487
|
-
context = block.input.pattern.slice(0, 20);
|
|
488
|
-
} else if (block.input.query) {
|
|
489
|
-
context = block.input.query.slice(0, 25);
|
|
490
|
-
} else if (block.input.url) {
|
|
491
|
-
try {
|
|
492
|
-
context = new URL(block.input.url).hostname;
|
|
493
|
-
} catch { context = 'web'; }
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const status = context
|
|
498
|
-
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
499
|
-
: `${displayEmoji} ${displayName}...`;
|
|
500
|
-
|
|
501
|
-
if (onStatus) {
|
|
502
|
-
onStatus(status).catch(() => { });
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
608
|
+
if (event.type === 'error') {
|
|
609
|
+
classifiedError = event;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (event.type === 'text' && event.text) {
|
|
613
|
+
finalResult = String(event.text);
|
|
614
|
+
if (waitingForTool) {
|
|
615
|
+
waitingForTool = false;
|
|
616
|
+
resetIdleTimer();
|
|
617
|
+
}
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (event.type === 'done') {
|
|
621
|
+
finalUsage = event.usage || null;
|
|
622
|
+
if (waitingForTool) {
|
|
623
|
+
waitingForTool = false;
|
|
624
|
+
resetIdleTimer();
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (event.type === 'tool_result') {
|
|
629
|
+
if (waitingForTool) {
|
|
630
|
+
waitingForTool = false;
|
|
631
|
+
resetIdleTimer();
|
|
506
632
|
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (event.type !== 'tool_use') continue;
|
|
636
|
+
|
|
637
|
+
toolCallCount++;
|
|
638
|
+
waitingForTool = true;
|
|
639
|
+
resetIdleTimer();
|
|
640
|
+
const toolName = event.toolName || 'Tool';
|
|
641
|
+
const toolInput = event.toolInput || {};
|
|
642
|
+
|
|
643
|
+
const toolEntry = { tool: toolName };
|
|
644
|
+
if (toolName === 'Skill' && toolInput.skill) toolEntry.skill = toolInput.skill;
|
|
645
|
+
else if (toolInput.command) toolEntry.context = String(toolInput.command).slice(0, 50);
|
|
646
|
+
else if (toolInput.file_path) toolEntry.context = path.basename(String(toolInput.file_path));
|
|
647
|
+
if (toolUsageLog.length < 50) toolUsageLog.push(toolEntry);
|
|
648
|
+
|
|
649
|
+
if (toolName === 'Write' && toolInput.file_path) {
|
|
650
|
+
const filePath = String(toolInput.file_path);
|
|
651
|
+
if (!writtenFiles.includes(filePath)) writtenFiles.push(filePath);
|
|
507
652
|
}
|
|
508
653
|
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
|
|
654
|
+
const now = Date.now();
|
|
655
|
+
if (now - lastStatusTime < STATUS_THROTTLE) continue;
|
|
656
|
+
lastStatusTime = now;
|
|
657
|
+
|
|
658
|
+
const emoji = TOOL_EMOJI[toolName] || TOOL_EMOJI.default;
|
|
659
|
+
let displayName = toolName;
|
|
660
|
+
let displayEmoji = emoji;
|
|
661
|
+
let context = '';
|
|
662
|
+
|
|
663
|
+
if (toolName === 'Skill' && toolInput.skill) {
|
|
664
|
+
context = toolInput.skill;
|
|
665
|
+
} else if (toolName === 'Task' && toolInput.description) {
|
|
666
|
+
context = String(toolInput.description).slice(0, 30);
|
|
667
|
+
} else if (toolName.startsWith('mcp__')) {
|
|
668
|
+
const parts = toolName.split('__');
|
|
669
|
+
const server = parts[1] || 'unknown';
|
|
670
|
+
const action = parts.slice(2).join('_') || '';
|
|
671
|
+
if (server === 'playwright') {
|
|
672
|
+
displayEmoji = '🌐';
|
|
673
|
+
displayName = 'Browser';
|
|
674
|
+
context = action.replace(/_/g, ' ');
|
|
675
|
+
} else {
|
|
676
|
+
displayEmoji = '🔗';
|
|
677
|
+
displayName = `MCP:${server}`;
|
|
678
|
+
context = action.replace(/_/g, ' ').slice(0, 25);
|
|
679
|
+
}
|
|
680
|
+
} else if (toolInput.file_path) {
|
|
681
|
+
const basename = path.basename(String(toolInput.file_path));
|
|
682
|
+
const dotIdx = basename.lastIndexOf('.');
|
|
683
|
+
context = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
684
|
+
} else if (toolInput.command) {
|
|
685
|
+
context = String(toolInput.command).slice(0, 30);
|
|
686
|
+
if (String(toolInput.command).length > 30) context += '...';
|
|
687
|
+
} else if (toolInput.pattern) {
|
|
688
|
+
context = String(toolInput.pattern).slice(0, 20);
|
|
689
|
+
} else if (toolInput.query) {
|
|
690
|
+
context = String(toolInput.query).slice(0, 25);
|
|
691
|
+
} else if (toolInput.url) {
|
|
692
|
+
try { context = new URL(toolInput.url).hostname; } catch { context = 'web'; }
|
|
512
693
|
}
|
|
513
|
-
|
|
514
|
-
|
|
694
|
+
|
|
695
|
+
const status = context
|
|
696
|
+
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
697
|
+
: `${displayEmoji} ${displayName}...`;
|
|
698
|
+
if (onStatus) onStatus(status).catch(() => { });
|
|
515
699
|
}
|
|
516
700
|
}
|
|
517
701
|
});
|
|
518
702
|
|
|
519
703
|
child.stderr.on('data', (data) => {
|
|
520
704
|
resetIdleTimer();
|
|
521
|
-
|
|
705
|
+
const chunk = data.toString();
|
|
706
|
+
stderr += chunk;
|
|
707
|
+
if (!classifiedError && typeof rt.classifyError === 'function') {
|
|
708
|
+
classifiedError = rt.classifyError(chunk);
|
|
709
|
+
}
|
|
522
710
|
});
|
|
523
711
|
|
|
524
712
|
child.on('close', (code) => {
|
|
@@ -527,34 +715,41 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
527
715
|
clearTimeout(sigkillTimer);
|
|
528
716
|
clearInterval(milestoneTimer);
|
|
529
717
|
|
|
530
|
-
// Process any remaining buffer
|
|
531
718
|
if (buffer.trim()) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (event.type === '
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
719
|
+
const events = parseEventsFromLine(buffer.trim());
|
|
720
|
+
for (const event of events) {
|
|
721
|
+
if (event.type === 'text' && event.text) finalResult = String(event.text);
|
|
722
|
+
if (event.type === 'done') finalUsage = event.usage || null;
|
|
723
|
+
if (event.type === 'session' && event.sessionId) observedSessionId = String(event.sessionId);
|
|
724
|
+
if (event.type === 'error') classifiedError = event;
|
|
725
|
+
}
|
|
538
726
|
}
|
|
539
727
|
|
|
540
|
-
// Clean up active process tracking
|
|
541
728
|
const proc = chatId ? activeProcesses.get(chatId) : null;
|
|
542
729
|
const wasAborted = proc && proc.aborted;
|
|
543
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
730
|
+
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
544
731
|
|
|
545
732
|
if (wasAborted) {
|
|
546
|
-
|
|
547
|
-
|
|
733
|
+
finalize({ output: finalResult || null, error: 'Stopped by user', files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (killed) {
|
|
548
737
|
const elapsed = Math.round((Date.now() - startTime) / 60000);
|
|
738
|
+
const idleMin = Math.max(1, Math.round(IDLE_TIMEOUT_MS / 60000));
|
|
549
739
|
const reason = killedReason === 'ceiling'
|
|
550
|
-
? `⏱ 已运行 ${elapsed}
|
|
551
|
-
: `⏱ 已
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
740
|
+
? `⏱ 已运行 ${elapsed} 分钟,达到上限(${Math.round(HARD_CEILING_MS / 60000)} 分钟)`
|
|
741
|
+
: `⏱ 已 ${idleMin} 分钟无输出,判定卡死(共运行 ${elapsed} 分钟)`;
|
|
742
|
+
finalize({ output: finalResult || null, error: reason, timedOut: true, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (code !== 0) {
|
|
746
|
+
const engineErr = classifiedError && classifiedError.message
|
|
747
|
+
? classifiedError.message
|
|
748
|
+
: (stderr || `Exit code ${code}`);
|
|
749
|
+
finalize({ output: finalResult || null, error: engineErr, errorCode: classifiedError ? classifiedError.code : undefined, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
750
|
+
return;
|
|
557
751
|
}
|
|
752
|
+
finalize({ output: finalResult || '', error: null, files: writtenFiles, toolUsageLog, usage: finalUsage, sessionId: observedSessionId || '' });
|
|
558
753
|
});
|
|
559
754
|
|
|
560
755
|
child.on('error', (err) => {
|
|
@@ -562,13 +757,28 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
562
757
|
clearTimeout(ceilingTimer);
|
|
563
758
|
clearTimeout(sigkillTimer);
|
|
564
759
|
clearInterval(milestoneTimer);
|
|
565
|
-
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
566
|
-
|
|
760
|
+
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
761
|
+
finalize({ output: null, error: formatEngineSpawnError(err, rt), files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
567
762
|
});
|
|
568
763
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
764
|
+
try {
|
|
765
|
+
child.stdin.write(input);
|
|
766
|
+
child.stdin.end();
|
|
767
|
+
} catch (e) {
|
|
768
|
+
clearTimeout(idleTimer);
|
|
769
|
+
clearTimeout(ceilingTimer);
|
|
770
|
+
clearTimeout(sigkillTimer);
|
|
771
|
+
clearInterval(milestoneTimer);
|
|
772
|
+
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
773
|
+
try { child.stdin.destroy(); } catch { /* ignore */ }
|
|
774
|
+
try {
|
|
775
|
+
const sig = rt.killSignal || 'SIGTERM';
|
|
776
|
+
process.kill(-child.pid, sig);
|
|
777
|
+
} catch {
|
|
778
|
+
try { child.kill(rt.killSignal || 'SIGTERM'); } catch { /* ignore */ }
|
|
779
|
+
}
|
|
780
|
+
finalize({ output: null, error: e.message, files: [], toolUsageLog: [], usage: null, sessionId: '' });
|
|
781
|
+
}
|
|
572
782
|
});
|
|
573
783
|
}
|
|
574
784
|
|
|
@@ -578,7 +788,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
578
788
|
if (!messageId || !session || !session.id) return;
|
|
579
789
|
const st = loadState();
|
|
580
790
|
if (!st.msg_sessions) st.msg_sessions = {};
|
|
581
|
-
st.msg_sessions[messageId] = { id: session.id, cwd: session.cwd };
|
|
791
|
+
st.msg_sessions[messageId] = { id: session.id, cwd: session.cwd, engine: session.engine || getDefaultEngine() };
|
|
582
792
|
const keys = Object.keys(st.msg_sessions);
|
|
583
793
|
if (keys.length > 200) {
|
|
584
794
|
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
@@ -632,6 +842,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
632
842
|
bot.sendTyping(chatId).catch(() => { });
|
|
633
843
|
}, 4000);
|
|
634
844
|
|
|
845
|
+
// Top-level safety net: any uncaught error inside askClaude MUST clean up timers and notify user.
|
|
846
|
+
// Without this, a ReferenceError / TypeError in the routing or injection code would silently
|
|
847
|
+
// kill the handler, leaving the typing indicator spinning forever.
|
|
848
|
+
try { // ── safety-net-start ──
|
|
849
|
+
|
|
635
850
|
// Agent nickname routing: "贾维斯" / "小美,帮我..." → switch project session
|
|
636
851
|
// Strict chats (chat_agent_map bound groups) must NOT switch agents via nickname
|
|
637
852
|
const _strictAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
@@ -640,7 +855,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
640
855
|
if (agentMatch) {
|
|
641
856
|
const { key, proj, rest } = agentMatch;
|
|
642
857
|
const projCwd = normalizeCwd(proj.cwd);
|
|
643
|
-
attachOrCreateSession(chatId, projCwd, proj.name || key);
|
|
858
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine ? normalizeEngineName(proj.engine) : getDefaultEngine());
|
|
644
859
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
645
860
|
if (!rest) {
|
|
646
861
|
// Pure nickname call — confirm switch and stop
|
|
@@ -653,41 +868,19 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
653
868
|
}
|
|
654
869
|
|
|
655
870
|
// Skill routing: detect skill first, then decide session
|
|
656
|
-
// BUT: if agent
|
|
657
|
-
|
|
871
|
+
// BUT: skip skill routing if agent addressed by nickname OR chat already has an active session
|
|
872
|
+
// (active conversation should never be hijacked by keyword-based skill matching)
|
|
873
|
+
let session = getSession(chatId);
|
|
874
|
+
const hasActiveSession = session && session.started;
|
|
875
|
+
const skill = (agentMatch || hasActiveSession) ? null : routeSkill(prompt);
|
|
658
876
|
const chatIdStr = String(chatId);
|
|
659
877
|
const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
660
878
|
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
661
879
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
662
880
|
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
881
|
+
const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
|
|
663
882
|
|
|
664
|
-
|
|
665
|
-
const PINNED_SKILL_SESSIONS = new Set(['skill-manager']);
|
|
666
|
-
const usePinnedSkillSession = !!(skill && PINNED_SKILL_SESSIONS.has(skill));
|
|
667
|
-
|
|
668
|
-
let session = getSession(chatId);
|
|
669
|
-
|
|
670
|
-
if (usePinnedSkillSession) {
|
|
671
|
-
// Use a dedicated long-lived session per skill
|
|
672
|
-
const state = loadState();
|
|
673
|
-
if (!state.pinned_sessions) state.pinned_sessions = {};
|
|
674
|
-
const pinned = state.pinned_sessions[skill];
|
|
675
|
-
if (pinned) {
|
|
676
|
-
// Reuse existing pinned session
|
|
677
|
-
state.sessions[chatId] = { id: pinned.id, cwd: pinned.cwd, started: true };
|
|
678
|
-
saveState(state);
|
|
679
|
-
session = state.sessions[chatId];
|
|
680
|
-
log('INFO', `Pinned session reused for skill ${skill}: ${pinned.id.slice(0, 8)}`);
|
|
681
|
-
} else {
|
|
682
|
-
// First time — create session and pin it
|
|
683
|
-
session = createSession(chatId, HOME, skill);
|
|
684
|
-
const st2 = loadState();
|
|
685
|
-
if (!st2.pinned_sessions) st2.pinned_sessions = {};
|
|
686
|
-
st2.pinned_sessions[skill] = { id: session.id, cwd: session.cwd };
|
|
687
|
-
saveState(st2);
|
|
688
|
-
log('INFO', `Pinned session created for skill ${skill}: ${session.id.slice(0, 8)}`);
|
|
689
|
-
}
|
|
690
|
-
} else if (!session) {
|
|
883
|
+
if (!session) {
|
|
691
884
|
if (boundCwd) {
|
|
692
885
|
// Agent-bound chats must stay in their own workspace: never attach to another project's session.
|
|
693
886
|
const recentInBound = listRecentSessions(1, boundCwd);
|
|
@@ -698,12 +891,13 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
698
891
|
id: target.sessionId,
|
|
699
892
|
cwd: boundCwd,
|
|
700
893
|
started: true,
|
|
894
|
+
engine: boundEngineName,
|
|
701
895
|
};
|
|
702
896
|
saveState(state);
|
|
703
897
|
session = state.sessions[chatId];
|
|
704
898
|
log('INFO', `Auto-attached ${chatId} to bound-session: ${target.sessionId.slice(0, 8)} (${path.basename(boundCwd)})`);
|
|
705
899
|
} else {
|
|
706
|
-
session = createSession(chatId, boundCwd, boundProject && boundProject.name ? boundProject.name : '');
|
|
900
|
+
session = createSession(chatId, boundCwd, boundProject && boundProject.name ? boundProject.name : '', boundEngineName);
|
|
707
901
|
log('INFO', `Created fresh session for bound workspace: ${path.basename(boundCwd)}`);
|
|
708
902
|
}
|
|
709
903
|
} else {
|
|
@@ -716,62 +910,94 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
716
910
|
id: target.sessionId,
|
|
717
911
|
cwd: target.projectPath,
|
|
718
912
|
started: true,
|
|
913
|
+
engine: getDefaultEngine(),
|
|
719
914
|
};
|
|
720
915
|
saveState(state);
|
|
721
916
|
session = state.sessions[chatId];
|
|
722
917
|
log('INFO', `Auto-attached ${chatId} to recent session: ${target.sessionId.slice(0, 8)} (${path.basename(target.projectPath)})`);
|
|
723
918
|
} else {
|
|
724
|
-
session = createSession(chatId);
|
|
919
|
+
session = createSession(chatId, undefined, '', boundEngineName);
|
|
725
920
|
}
|
|
726
921
|
}
|
|
727
922
|
}
|
|
728
923
|
|
|
924
|
+
const engineName = normalizeEngineName(
|
|
925
|
+
(boundProject && boundProject.engine)
|
|
926
|
+
|| (session && session.engine)
|
|
927
|
+
|| getDefaultEngine()
|
|
928
|
+
);
|
|
929
|
+
const runtime = getEngineRuntime(engineName);
|
|
930
|
+
session.engine = engineName;
|
|
931
|
+
if (!getSession(chatId) || getSession(chatId).engine !== engineName) {
|
|
932
|
+
await patchSessionSerialized(chatId, (cur) => ({
|
|
933
|
+
...cur,
|
|
934
|
+
engine: engineName,
|
|
935
|
+
cwd: session.cwd || cur.cwd || HOME,
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
938
|
+
|
|
729
939
|
// Safety guard: prevent stale state from resuming another workspace's session.
|
|
730
|
-
if (
|
|
940
|
+
if (engineName === 'claude' && session && session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
731
941
|
const sessionCwd = normalizeCwd(session.cwd);
|
|
732
942
|
const existsInCwd = isSessionInCwd(session.id, sessionCwd);
|
|
733
943
|
if (!existsInCwd) {
|
|
734
944
|
log('WARN', `Session mismatch detected for ${chatId}: ${session.id.slice(0, 8)} not found in ${sessionCwd}; creating fresh session`);
|
|
735
|
-
session = createSession(chatId, sessionCwd, boundProject && boundProject.name ? boundProject.name : '');
|
|
945
|
+
session = createSession(chatId, sessionCwd, boundProject && boundProject.name ? boundProject.name : '', engineName);
|
|
736
946
|
}
|
|
737
947
|
}
|
|
738
948
|
|
|
739
|
-
// Build claude command
|
|
740
|
-
const args = ['-p'];
|
|
741
949
|
const daemonCfg = loadConfig().daemon || {};
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
950
|
+
const mentorCfg = (daemonCfg.mentor && typeof daemonCfg.mentor === 'object') ? daemonCfg.mentor : {};
|
|
951
|
+
const mentorEnabled = !!(mentorEngine && mentorCfg.enabled);
|
|
952
|
+
const excludeAgents = new Set(
|
|
953
|
+
(Array.isArray(mentorCfg.exclude_agents) ? mentorCfg.exclude_agents : [])
|
|
954
|
+
.map(x => String(x || '').trim())
|
|
955
|
+
.filter(Boolean)
|
|
956
|
+
);
|
|
957
|
+
const chatAgentKey = boundProjectKey || 'personal';
|
|
958
|
+
const mentorExcluded = excludeAgents.has(chatAgentKey);
|
|
959
|
+
let mentorSuppressed = false;
|
|
960
|
+
|
|
961
|
+
// Mentor pre-flight breaker: first hit sends a short reassurance; cooldown does not block normal answers.
|
|
962
|
+
if (mentorEnabled && !mentorExcluded) {
|
|
963
|
+
try {
|
|
964
|
+
const breaker = mentorEngine.checkEmotionBreaker(prompt, mentorCfg);
|
|
965
|
+
if (breaker && breaker.tripped) {
|
|
966
|
+
mentorSuppressed = true;
|
|
967
|
+
if (breaker.reason !== 'cooldown_active' && breaker.response) {
|
|
968
|
+
await bot.sendMessage(chatId, breaker.response).catch(() => { });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch (e) {
|
|
972
|
+
log('WARN', `Mentor breaker failed: ${e.message}`);
|
|
973
|
+
}
|
|
760
974
|
}
|
|
761
975
|
|
|
976
|
+
// Build engine command
|
|
977
|
+
const model = (boundProject && boundProject.model) || daemonCfg.model || runtime.defaultModel;
|
|
978
|
+
const args = runtime.buildArgs({
|
|
979
|
+
model,
|
|
980
|
+
readOnly,
|
|
981
|
+
daemonCfg,
|
|
982
|
+
session,
|
|
983
|
+
cwd: session.cwd,
|
|
984
|
+
});
|
|
985
|
+
|
|
762
986
|
// Memory & Knowledge Injection (RAG)
|
|
763
987
|
let memoryHint = '';
|
|
988
|
+
// projectKey must be declared outside the try block so the daemonHint template below can reference it.
|
|
989
|
+
const _cid0 = String(chatId);
|
|
990
|
+
const _agentMap0 = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
991
|
+
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
764
992
|
try {
|
|
765
993
|
const memory = require('./memory');
|
|
766
|
-
const _cid = String(chatId);
|
|
767
|
-
const _cfg = loadConfig();
|
|
768
|
-
const _agentMap = { ...(_cfg.telegram ? _cfg.telegram.chat_agent_map : {}), ...(_cfg.feishu ? _cfg.feishu.chat_agent_map : {}) };
|
|
769
|
-
const projectKey = _agentMap[_cid] || projectKeyFromVirtualChatId(_cid);
|
|
770
994
|
|
|
771
|
-
// L1: NOW.md
|
|
995
|
+
// L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
|
|
772
996
|
if (!session.started) {
|
|
773
997
|
try {
|
|
774
|
-
const
|
|
998
|
+
const nowDir = path.join(HOME, '.metame', 'memory', 'now');
|
|
999
|
+
const nowKey = projectKey || 'default';
|
|
1000
|
+
const nowPath = path.join(nowDir, `${nowKey}.md`);
|
|
775
1001
|
if (fs.existsSync(nowPath)) {
|
|
776
1002
|
const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
|
|
777
1003
|
if (nowContent) {
|
|
@@ -811,11 +1037,13 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
811
1037
|
|
|
812
1038
|
// ZPD: build competence hint from brain profile
|
|
813
1039
|
let zdpHint = '';
|
|
1040
|
+
let brainDoc = null;
|
|
814
1041
|
if (!session.started) {
|
|
815
1042
|
try {
|
|
816
1043
|
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
817
1044
|
if (fs.existsSync(brainPath)) {
|
|
818
1045
|
const brain = yaml.load(fs.readFileSync(brainPath, 'utf8'));
|
|
1046
|
+
brainDoc = brain;
|
|
819
1047
|
const cmap = brain && brain.user_competence_map;
|
|
820
1048
|
if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
|
|
821
1049
|
const lines = Object.entries(cmap)
|
|
@@ -826,6 +1054,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
826
1054
|
}
|
|
827
1055
|
} catch { /* non-critical */ }
|
|
828
1056
|
}
|
|
1057
|
+
if (!brainDoc) {
|
|
1058
|
+
try {
|
|
1059
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1060
|
+
if (fs.existsSync(brainPath)) brainDoc = yaml.load(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1061
|
+
} catch { /* ignore */ }
|
|
1062
|
+
}
|
|
829
1063
|
|
|
830
1064
|
// Inject daemon hints only on first message of a session
|
|
831
1065
|
// Task-specific rules (3-5) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
@@ -841,9 +1075,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
841
1075
|
Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
|
|
842
1076
|
Only write verified facts. Do not write speculative or process-description entries.
|
|
843
1077
|
When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"
|
|
844
|
-
5. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/
|
|
845
|
-
\`printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/
|
|
846
|
-
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/
|
|
1078
|
+
5. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/now/${projectKey || 'default'}.md using:
|
|
1079
|
+
\`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
|
|
1080
|
+
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
847
1081
|
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
848
1082
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
849
1083
|
2. File sending: User is on MOBILE. When they ask to see/download a file:
|
|
@@ -852,7 +1086,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
852
1086
|
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
853
1087
|
- Keep response brief: "请查收~! [[FILE:/path/to/file]]"
|
|
854
1088
|
- Multiple files: use multiple [[FILE:...]] tags${zdpHint ? '\n Explanation depth (ZPD):\n' + zdpHint : ''}${taskRules}]`;
|
|
855
|
-
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
|
|
856
1092
|
|
|
857
1093
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
858
1094
|
|
|
@@ -888,9 +1124,61 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
888
1124
|
} catch { /* non-critical */ }
|
|
889
1125
|
}
|
|
890
1126
|
|
|
1127
|
+
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1128
|
+
let mentorHint = '';
|
|
1129
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
1130
|
+
try {
|
|
1131
|
+
const signals = collectRecentSessionSignals(session.id, 6);
|
|
1132
|
+
let skeleton = null;
|
|
1133
|
+
if (sessionAnalytics && typeof sessionAnalytics.extractSkeleton === 'function') {
|
|
1134
|
+
const file = findSessionFile(session.id);
|
|
1135
|
+
if (file && fs.existsSync(file)) {
|
|
1136
|
+
const st = fs.statSync(file);
|
|
1137
|
+
if (st.size <= 2 * 1024 * 1024) {
|
|
1138
|
+
skeleton = sessionAnalytics.extractSkeleton(file);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const zone = skeleton && mentorEngine.computeZone
|
|
1143
|
+
? mentorEngine.computeZone(skeleton).zone
|
|
1144
|
+
: 'stretch';
|
|
1145
|
+
const sessionState = {
|
|
1146
|
+
zone,
|
|
1147
|
+
recentMessages: signals.recentMessages,
|
|
1148
|
+
cwd: session.cwd,
|
|
1149
|
+
skeleton,
|
|
1150
|
+
sessionStartTime: signals.sessionStartTime || new Date().toISOString(),
|
|
1151
|
+
topic: String(prompt || '').slice(0, 120),
|
|
1152
|
+
currentTopic: String(prompt || '').slice(0, 120),
|
|
1153
|
+
lastUserMessage: String(prompt || '').slice(0, 200),
|
|
1154
|
+
};
|
|
1155
|
+
const built = mentorEngine.buildMentorPrompt(sessionState, brainDoc || {}, mentorCfg);
|
|
1156
|
+
if (built && String(built).trim()) mentorHint = `\n\n${String(built).trim()}`;
|
|
1157
|
+
|
|
1158
|
+
// Collect reflection debt: if user returns to same project+topic, inject recall prompt.
|
|
1159
|
+
// Suppressed by quiet_until (user explicitly asked for silence), but NOT by expert skip
|
|
1160
|
+
// (even experts may not have reviewed AI-generated code).
|
|
1161
|
+
const quietUntil = brainDoc && brainDoc.growth ? brainDoc.growth.quiet_until : null;
|
|
1162
|
+
const quietMs = quietUntil ? new Date(quietUntil).getTime() : 0;
|
|
1163
|
+
const isQuiet = quietMs && quietMs > Date.now();
|
|
1164
|
+
if (!isQuiet && mentorEngine.collectDebt) {
|
|
1165
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1166
|
+
const projectId = info && info.project_id ? info.project_id : '';
|
|
1167
|
+
if (projectId) {
|
|
1168
|
+
const debt = mentorEngine.collectDebt(projectId, String(prompt || '').slice(0, 120));
|
|
1169
|
+
if (debt && debt.prompt) {
|
|
1170
|
+
mentorHint += `\n\n[Reflection debt] ${debt.prompt}`;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} catch (e) {
|
|
1175
|
+
log('WARN', `Mentor prompt build failed: ${e.message}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
891
1179
|
// Always append a compact language guard to prevent accidental Korean/Japanese responses
|
|
892
1180
|
const langGuard = '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
893
|
-
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint + langGuard;
|
|
1181
|
+
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
894
1182
|
|
|
895
1183
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
896
1184
|
// Pass the user prompt as label so checkpoint list is human-readable
|
|
@@ -916,9 +1204,101 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
916
1204
|
} catch { /* ignore status update failures */ }
|
|
917
1205
|
};
|
|
918
1206
|
|
|
919
|
-
|
|
1207
|
+
const wasCodexResumeAttempt = runtime.name === 'codex'
|
|
1208
|
+
&& !!(session && session.started && session.id && session.id !== '__continue__');
|
|
1209
|
+
const onSession = async (nextSessionId) => {
|
|
1210
|
+
const safeNextId = String(nextSessionId || '').trim();
|
|
1211
|
+
if (!safeNextId) return;
|
|
1212
|
+
const prevSessionId = session && session.id ? String(session.id) : '';
|
|
1213
|
+
const wasStarted = !!(session && session.started);
|
|
1214
|
+
session = {
|
|
1215
|
+
...session,
|
|
1216
|
+
id: safeNextId,
|
|
1217
|
+
engine: runtime.name,
|
|
1218
|
+
started: true,
|
|
1219
|
+
};
|
|
1220
|
+
await patchSessionSerialized(chatId, (cur) => ({
|
|
1221
|
+
...cur,
|
|
1222
|
+
id: safeNextId,
|
|
1223
|
+
cwd: session.cwd || cur.cwd || HOME,
|
|
1224
|
+
engine: runtime.name,
|
|
1225
|
+
started: true,
|
|
1226
|
+
}));
|
|
1227
|
+
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
1228
|
+
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
let output, error, errorCode, files, toolUsageLog, timedOut, usage, sessionId;
|
|
920
1233
|
try {
|
|
921
|
-
({
|
|
1234
|
+
({
|
|
1235
|
+
output,
|
|
1236
|
+
error,
|
|
1237
|
+
errorCode,
|
|
1238
|
+
timedOut,
|
|
1239
|
+
files,
|
|
1240
|
+
toolUsageLog,
|
|
1241
|
+
usage,
|
|
1242
|
+
sessionId,
|
|
1243
|
+
} = await spawnClaudeStreaming(
|
|
1244
|
+
args,
|
|
1245
|
+
fullPrompt,
|
|
1246
|
+
session.cwd,
|
|
1247
|
+
onStatus,
|
|
1248
|
+
600000,
|
|
1249
|
+
chatId,
|
|
1250
|
+
boundProjectKey || '',
|
|
1251
|
+
runtime,
|
|
1252
|
+
onSession,
|
|
1253
|
+
));
|
|
1254
|
+
|
|
1255
|
+
if (sessionId) await onSession(sessionId);
|
|
1256
|
+
|
|
1257
|
+
if (shouldRetryCodexResumeFallback({
|
|
1258
|
+
runtimeName: runtime.name,
|
|
1259
|
+
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1260
|
+
output,
|
|
1261
|
+
error,
|
|
1262
|
+
errorCode,
|
|
1263
|
+
canRetry: canRetryCodexResume(chatId),
|
|
1264
|
+
})) {
|
|
1265
|
+
markCodexResumeRetried(chatId);
|
|
1266
|
+
log('WARN', `Codex resume failed for ${chatId}, retrying once with fresh exec: ${String(error).slice(0, 120)}`);
|
|
1267
|
+
session = createSession(
|
|
1268
|
+
chatId,
|
|
1269
|
+
session.cwd,
|
|
1270
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1271
|
+
'codex'
|
|
1272
|
+
);
|
|
1273
|
+
const retryArgs = runtime.buildArgs({
|
|
1274
|
+
model,
|
|
1275
|
+
readOnly,
|
|
1276
|
+
daemonCfg,
|
|
1277
|
+
session,
|
|
1278
|
+
cwd: session.cwd,
|
|
1279
|
+
});
|
|
1280
|
+
({
|
|
1281
|
+
output,
|
|
1282
|
+
error,
|
|
1283
|
+
errorCode,
|
|
1284
|
+
timedOut,
|
|
1285
|
+
files,
|
|
1286
|
+
toolUsageLog,
|
|
1287
|
+
usage,
|
|
1288
|
+
sessionId,
|
|
1289
|
+
} = await spawnClaudeStreaming(
|
|
1290
|
+
retryArgs,
|
|
1291
|
+
fullPrompt,
|
|
1292
|
+
session.cwd,
|
|
1293
|
+
onStatus,
|
|
1294
|
+
600000,
|
|
1295
|
+
chatId,
|
|
1296
|
+
boundProjectKey || '',
|
|
1297
|
+
runtime,
|
|
1298
|
+
onSession,
|
|
1299
|
+
));
|
|
1300
|
+
if (sessionId) await onSession(sessionId);
|
|
1301
|
+
}
|
|
922
1302
|
} catch (spawnErr) {
|
|
923
1303
|
clearInterval(typingTimer);
|
|
924
1304
|
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
@@ -944,6 +1324,24 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
944
1324
|
bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
945
1325
|
}
|
|
946
1326
|
|
|
1327
|
+
// Mentor post-flight debt registration (intense mode only).
|
|
1328
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed && mentorEngine && typeof mentorEngine.registerDebt === 'function' && output) {
|
|
1329
|
+
try {
|
|
1330
|
+
const mode = resolveMentorMode(mentorCfg);
|
|
1331
|
+
if (mode === 'intense') {
|
|
1332
|
+
const codeLines = countCodeLines(output);
|
|
1333
|
+
if (codeLines > 30) {
|
|
1334
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1335
|
+
const projectId = info && info.project_id ? info.project_id : 'proj_default';
|
|
1336
|
+
mentorEngine.registerDebt(projectId, String(prompt || '').slice(0, 120), codeLines);
|
|
1337
|
+
log('INFO', `[MENTOR] Registered reflection debt (${projectId}, lines=${codeLines})`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
log('WARN', `Mentor post-flight failed: ${e.message}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
947
1345
|
// When Claude completes with no text output (pure tool work), send a done notice
|
|
948
1346
|
if (output === '' && !error) {
|
|
949
1347
|
// Special case: if dispatch_to was called, send a "forwarded" confirmation
|
|
@@ -969,19 +1367,22 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
969
1367
|
}
|
|
970
1368
|
|
|
971
1369
|
if (output) {
|
|
1370
|
+
if (runtime.name === 'codex') _codexResumeRetryTs.delete(String(chatId));
|
|
972
1371
|
// Detect provider/model errors disguised as output (e.g., "model not found", API errors)
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1372
|
+
if (runtime.name === 'claude') {
|
|
1373
|
+
const activeProvCheck = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
1374
|
+
const builtinModelsCheck = ['sonnet', 'opus', 'haiku'];
|
|
1375
|
+
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
1376
|
+
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
1377
|
+
try {
|
|
1378
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
1379
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
1380
|
+
} catch (fbErr) {
|
|
1381
|
+
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
1382
|
+
await bot.sendMarkdown(chatId, output);
|
|
1383
|
+
}
|
|
1384
|
+
return { ok: false, error: output };
|
|
983
1385
|
}
|
|
984
|
-
return { ok: false, error: output };
|
|
985
1386
|
}
|
|
986
1387
|
|
|
987
1388
|
// Mark session as started after first successful call
|
|
@@ -1042,28 +1443,42 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1042
1443
|
}
|
|
1043
1444
|
|
|
1044
1445
|
// Auto-name: if this was the first message and session has no name, generate one
|
|
1045
|
-
if (wasNew && !getSessionName(session.id)) {
|
|
1446
|
+
if (runtime.name === 'claude' && wasNew && !getSessionName(session.id)) {
|
|
1046
1447
|
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
1047
1448
|
}
|
|
1048
1449
|
return { ok: !timedOut };
|
|
1049
1450
|
} else {
|
|
1050
1451
|
const errMsg = error || 'Unknown error';
|
|
1051
|
-
|
|
1452
|
+
const userErrMsg = (errorCode === 'AUTH_REQUIRED' || errorCode === 'RATE_LIMIT')
|
|
1453
|
+
? errMsg
|
|
1454
|
+
: `Error: ${errMsg.slice(0, 200)}`;
|
|
1455
|
+
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
1052
1456
|
|
|
1053
|
-
// If session not found (expired/deleted), create new and retry once
|
|
1054
|
-
if (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use')) {
|
|
1457
|
+
// If session not found (expired/deleted), create new and retry once (Claude path)
|
|
1458
|
+
if (runtime.name === 'claude' && (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use'))) {
|
|
1055
1459
|
log('WARN', `Session ${session.id} unusable (${errMsg.includes('already in use') ? 'locked' : 'not found'}), creating new`);
|
|
1056
|
-
session = createSession(chatId, session.cwd);
|
|
1057
|
-
|
|
1058
|
-
const retryArgs =
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
const retry = await spawnClaudeStreaming(
|
|
1460
|
+
session = createSession(chatId, session.cwd, '', runtime.name);
|
|
1461
|
+
|
|
1462
|
+
const retryArgs = runtime.buildArgs({
|
|
1463
|
+
model,
|
|
1464
|
+
readOnly,
|
|
1465
|
+
daemonCfg,
|
|
1466
|
+
session,
|
|
1467
|
+
cwd: session.cwd,
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
const retry = await spawnClaudeStreaming(
|
|
1471
|
+
retryArgs,
|
|
1472
|
+
fullPrompt,
|
|
1473
|
+
session.cwd,
|
|
1474
|
+
onStatus,
|
|
1475
|
+
600000,
|
|
1476
|
+
chatId,
|
|
1477
|
+
boundProjectKey || '',
|
|
1478
|
+
runtime,
|
|
1479
|
+
onSession,
|
|
1480
|
+
);
|
|
1481
|
+
if (retry.sessionId) await onSession(retry.sessionId);
|
|
1067
1482
|
if (retry.output) {
|
|
1068
1483
|
markSessionStarted(chatId);
|
|
1069
1484
|
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
@@ -1072,27 +1487,39 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1072
1487
|
return { ok: true };
|
|
1073
1488
|
} else {
|
|
1074
1489
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
1075
|
-
try { await bot.sendMessage(chatId,
|
|
1490
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1076
1491
|
return { ok: false, error: retry.error || errMsg };
|
|
1077
1492
|
}
|
|
1078
1493
|
} else {
|
|
1079
|
-
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1494
|
+
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus (Claude path only)
|
|
1495
|
+
if (runtime.name === 'claude') {
|
|
1496
|
+
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
1497
|
+
const builtinModels = ['sonnet', 'opus', 'haiku'];
|
|
1498
|
+
if (activeProv !== 'anthropic' || !builtinModels.includes(model)) {
|
|
1499
|
+
try {
|
|
1500
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
1501
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
1502
|
+
} catch (fallbackErr) {
|
|
1503
|
+
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
1504
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1089
1508
|
}
|
|
1090
1509
|
} else {
|
|
1091
|
-
try { await bot.sendMessage(chatId,
|
|
1510
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1092
1511
|
}
|
|
1093
|
-
return { ok: false, error: errMsg };
|
|
1512
|
+
return { ok: false, error: errMsg, errorCode };
|
|
1094
1513
|
}
|
|
1095
1514
|
}
|
|
1515
|
+
|
|
1516
|
+
} catch (fatalErr) { // ── safety-net-catch ──
|
|
1517
|
+
clearInterval(typingTimer);
|
|
1518
|
+
if (statusMsgId && bot.deleteMessage) await bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
1519
|
+
log('FATAL', `[askClaude] Uncaught error for ${chatId}: ${fatalErr.message}\n${fatalErr.stack}`);
|
|
1520
|
+
try { await bot.sendMessage(chatId, `❌ 内部错误: ${fatalErr.message}`); } catch { /* */ }
|
|
1521
|
+
return { ok: false, error: fatalErr.message };
|
|
1522
|
+
}
|
|
1096
1523
|
}
|
|
1097
1524
|
|
|
1098
1525
|
return {
|
|
@@ -1102,6 +1529,15 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1102
1529
|
spawnClaudeStreaming,
|
|
1103
1530
|
trackMsgSession,
|
|
1104
1531
|
askClaude,
|
|
1532
|
+
_private: {
|
|
1533
|
+
patchSessionSerialized,
|
|
1534
|
+
shouldRetryCodexResumeFallback,
|
|
1535
|
+
formatEngineSpawnError,
|
|
1536
|
+
adaptDaemonHintForEngine,
|
|
1537
|
+
canRetryCodexResume,
|
|
1538
|
+
markCodexResumeRetried,
|
|
1539
|
+
CODEX_RESUME_RETRY_WINDOW_MS,
|
|
1540
|
+
},
|
|
1105
1541
|
};
|
|
1106
1542
|
}
|
|
1107
1543
|
|