metame-cli 1.5.3 → 1.5.5
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 +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -2,9 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
const { classifyChatUsage } = require('./usage-classifier');
|
|
4
4
|
const { deriveProjectInfo } = require('./utils');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
createEngineRuntimeFactory,
|
|
7
|
+
normalizeEngineName,
|
|
8
|
+
resolveEngineModel,
|
|
9
|
+
ENGINE_MODEL_CONFIG,
|
|
10
|
+
_private: { resolveCodexPermissionProfile },
|
|
11
|
+
} = require('./daemon-engine-runtime');
|
|
12
|
+
const { buildIntentHintBlock } = require('./intent-registry');
|
|
6
13
|
const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
|
|
7
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Antigravity Raw Session Logging — Lossless Diary (L0)
|
|
17
|
+
* [PROTECTED] Append every user→AI turn to a daily markdown file.
|
|
18
|
+
* Isolated as a standalone function to prevent accidental deletion during edits.
|
|
19
|
+
*/
|
|
20
|
+
function logRawSessionDiary(fs, path, HOME, { chatId, prompt, output, error, projectKey }) {
|
|
21
|
+
try {
|
|
22
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
23
|
+
const ym = today.slice(0, 7); // YYYY-MM
|
|
24
|
+
const sessDir = path.join(HOME, '.metame', 'sessions', ym);
|
|
25
|
+
if (!fs.existsSync(sessDir)) fs.mkdirSync(sessDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const diaryPath = path.join(sessDir, `${today}_${chatId}.md`);
|
|
28
|
+
const MAX_OUTPUT_LOG = 8000;
|
|
29
|
+
const outputLog = (output || error || 'No output.').slice(0, MAX_OUTPUT_LOG);
|
|
30
|
+
const outputTruncated = (output || '').length > MAX_OUTPUT_LOG ? '\n\n[truncated]' : '';
|
|
31
|
+
const entry = `\n---\ndate: ${new Date().toISOString()}\nproject: ${projectKey || 'global'}\n---\n\n## 🙋♂️ 用户指令\n\`\`\`text\n${prompt}\n\`\`\`\n\n## 🤖 执行实录\n${outputLog}${outputTruncated}\n`;
|
|
32
|
+
fs.appendFileSync(diaryPath, entry, 'utf8');
|
|
33
|
+
} catch (e) { console.warn(`[MetaMe] Raw session logging failed: ${e.message}`); }
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
function createClaudeEngine(deps) {
|
|
9
37
|
const {
|
|
10
38
|
fs,
|
|
@@ -16,7 +44,6 @@ function createClaudeEngine(deps) {
|
|
|
16
44
|
getActiveProviderEnv,
|
|
17
45
|
activeProcesses,
|
|
18
46
|
saveActivePids,
|
|
19
|
-
messageQueue,
|
|
20
47
|
log,
|
|
21
48
|
yaml,
|
|
22
49
|
providerMod,
|
|
@@ -31,7 +58,6 @@ function createClaudeEngine(deps) {
|
|
|
31
58
|
isContentFile,
|
|
32
59
|
sendFileButtons,
|
|
33
60
|
findSessionFile,
|
|
34
|
-
listRecentSessions,
|
|
35
61
|
getSession,
|
|
36
62
|
getSessionForEngine,
|
|
37
63
|
createSession,
|
|
@@ -39,6 +65,9 @@ function createClaudeEngine(deps) {
|
|
|
39
65
|
writeSessionName,
|
|
40
66
|
markSessionStarted,
|
|
41
67
|
isEngineSessionValid,
|
|
68
|
+
getCodexSessionSandboxProfile,
|
|
69
|
+
getCodexSessionPermissionMode,
|
|
70
|
+
getSessionRecentContext,
|
|
42
71
|
gitCheckpoint,
|
|
43
72
|
gitCheckpointAsync,
|
|
44
73
|
recordTokens,
|
|
@@ -52,11 +81,42 @@ function createClaudeEngine(deps) {
|
|
|
52
81
|
function getDefaultEngine() {
|
|
53
82
|
return (typeof _getDefaultEngine === 'function') ? _getDefaultEngine() : 'claude';
|
|
54
83
|
}
|
|
84
|
+
function resolveSessionForEngine(chatId, engineName) {
|
|
85
|
+
if (typeof getSessionForEngine === 'function') {
|
|
86
|
+
return getSessionForEngine(chatId, engineName);
|
|
87
|
+
}
|
|
88
|
+
const legacy = typeof getSession === 'function' ? getSession(chatId) : null;
|
|
89
|
+
if (!legacy) return null;
|
|
90
|
+
if (!legacy.engines) return legacy;
|
|
91
|
+
const slot = legacy.engines[engineName] || null;
|
|
92
|
+
if (!slot) return null;
|
|
93
|
+
return {
|
|
94
|
+
...legacy,
|
|
95
|
+
...slot,
|
|
96
|
+
cwd: legacy.cwd || HOME,
|
|
97
|
+
engine: engineName,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function validateEngineSession(engineName, sessionId, cwd) {
|
|
101
|
+
if (typeof isEngineSessionValid === 'function') {
|
|
102
|
+
return isEngineSessionValid(engineName, sessionId, cwd);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
55
106
|
let mentorEngine = null;
|
|
56
107
|
try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
|
|
57
108
|
let sessionAnalytics = null;
|
|
58
109
|
try { sessionAnalytics = require('./session-analytics'); } catch { /* optional */ }
|
|
59
110
|
|
|
111
|
+
function shouldAutoRouteSkill({ agentMatch, hasActiveSession, boundProjectKey, skillName }) {
|
|
112
|
+
if (agentMatch || hasActiveSession) return false;
|
|
113
|
+
if (
|
|
114
|
+
String(boundProjectKey || '').trim() === 'personal'
|
|
115
|
+
&& String(skillName || '').trim() === 'macos-local-orchestrator'
|
|
116
|
+
) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
60
120
|
const getEngineRuntime = typeof injectedGetEngineRuntime === 'function'
|
|
61
121
|
? injectedGetEngineRuntime
|
|
62
122
|
: createEngineRuntimeFactory({ fs, path, HOME, CLAUDE_BIN, getActiveProviderEnv });
|
|
@@ -126,7 +186,11 @@ function createClaudeEngine(deps) {
|
|
|
126
186
|
if (!state.sessions) state.sessions = {};
|
|
127
187
|
const cur = state.sessions[chatId] || {};
|
|
128
188
|
const patched = typeof patchFn === 'function' ? patchFn(cur) : cur;
|
|
129
|
-
|
|
189
|
+
if (patched && typeof patched === 'object') {
|
|
190
|
+
state.sessions[chatId] = { ...patched, last_active: Date.now() };
|
|
191
|
+
} else {
|
|
192
|
+
state.sessions[chatId] = cur;
|
|
193
|
+
}
|
|
130
194
|
saveState(state);
|
|
131
195
|
}).catch((e) => {
|
|
132
196
|
log('WARN', `patchSessionSerialized failed for ${chatId}: ${e.message}`);
|
|
@@ -138,27 +202,35 @@ function createClaudeEngine(deps) {
|
|
|
138
202
|
}
|
|
139
203
|
|
|
140
204
|
const CODEX_RESUME_RETRY_WINDOW_MS = 10 * 60 * 1000;
|
|
141
|
-
const
|
|
205
|
+
const CODEX_PERMISSION_STABILIZE_MAX_RETRIES = 2;
|
|
206
|
+
const _codexResumeRetryTs = new Map(); // `${chatId}:${kind}` -> last retry ts
|
|
142
207
|
|
|
143
|
-
function
|
|
144
|
-
const
|
|
208
|
+
function getCodexResumeRetryKey(chatId, kind = 'default') {
|
|
209
|
+
const base = String(chatId || '').trim();
|
|
210
|
+
const mode = String(kind || 'default').trim();
|
|
211
|
+
return base && mode ? `${base}:${mode}` : '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function canRetryCodexResume(chatId, kind = 'default') {
|
|
215
|
+
const key = getCodexResumeRetryKey(chatId, kind);
|
|
145
216
|
if (!key) return false;
|
|
146
217
|
const last = Number(_codexResumeRetryTs.get(key) || 0);
|
|
147
218
|
if (!last) return true;
|
|
148
219
|
return (Date.now() - last) > CODEX_RESUME_RETRY_WINDOW_MS;
|
|
149
220
|
}
|
|
150
221
|
|
|
151
|
-
function markCodexResumeRetried(chatId) {
|
|
152
|
-
const key =
|
|
222
|
+
function markCodexResumeRetried(chatId, kind = 'default') {
|
|
223
|
+
const key = getCodexResumeRetryKey(chatId, kind);
|
|
153
224
|
if (!key) return;
|
|
154
225
|
_codexResumeRetryTs.set(key, Date.now());
|
|
155
226
|
}
|
|
156
227
|
|
|
157
|
-
function shouldRetryCodexResumeFallback({ runtimeName, wasResumeAttempt, output, error, errorCode, canRetry }) {
|
|
228
|
+
function shouldRetryCodexResumeFallback({ runtimeName, wasResumeAttempt, output, error, errorCode, canRetry, failureKind = '' }) {
|
|
158
229
|
return runtimeName === 'codex'
|
|
159
230
|
&& !!wasResumeAttempt
|
|
160
231
|
&& !!error
|
|
161
232
|
&& (!output || !!errorCode)
|
|
233
|
+
&& failureKind !== 'user-stop'
|
|
162
234
|
&& !!canRetry;
|
|
163
235
|
}
|
|
164
236
|
|
|
@@ -184,6 +256,218 @@ function createClaudeEngine(deps) {
|
|
|
184
256
|
return out;
|
|
185
257
|
}
|
|
186
258
|
|
|
259
|
+
function getCodexPermissionProfile(readOnly, daemonCfg = {}, session = {}) {
|
|
260
|
+
return resolveCodexPermissionProfile({ readOnly, daemonCfg, session });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getSessionChatId(chatId, boundProjectKey) {
|
|
264
|
+
const rawChatId = String(chatId || '');
|
|
265
|
+
if (rawChatId.startsWith('_agent_') || rawChatId.startsWith('_scope_')) return rawChatId;
|
|
266
|
+
if (boundProjectKey) return `_bound_${boundProjectKey}`;
|
|
267
|
+
return rawChatId || chatId;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeCodexSandboxMode(value, fallback = null) {
|
|
271
|
+
const text = String(value || '').trim().toLowerCase();
|
|
272
|
+
if (!text) return fallback;
|
|
273
|
+
if (text === 'read-only' || text === 'readonly') return 'read-only';
|
|
274
|
+
if (text === 'workspace-write' || text === 'workspace') return 'workspace-write';
|
|
275
|
+
if (
|
|
276
|
+
text === 'danger-full-access'
|
|
277
|
+
|| text === 'dangerous'
|
|
278
|
+
|| text === 'full-access'
|
|
279
|
+
|| text === 'full'
|
|
280
|
+
|| text === 'bypass'
|
|
281
|
+
|| text === 'writable'
|
|
282
|
+
) return 'danger-full-access';
|
|
283
|
+
return fallback;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeCodexApprovalPolicy(value, fallback = null) {
|
|
287
|
+
const text = String(value || '').trim().toLowerCase();
|
|
288
|
+
if (!text) return fallback;
|
|
289
|
+
if (text === 'never' || text === 'no' || text === 'none') return 'never';
|
|
290
|
+
if (text === 'on-failure' || text === 'on_failure' || text === 'failure') return 'on-failure';
|
|
291
|
+
if (text === 'on-request' || text === 'on_request' || text === 'request') return 'on-request';
|
|
292
|
+
if (text === 'untrusted') return 'untrusted';
|
|
293
|
+
return fallback;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeComparableCodexPermissionProfile(profile) {
|
|
297
|
+
if (!profile) return null;
|
|
298
|
+
const sandboxMode = normalizeCodexSandboxMode(
|
|
299
|
+
profile.sandboxMode || profile.permissionMode,
|
|
300
|
+
null
|
|
301
|
+
);
|
|
302
|
+
const approvalPolicy = normalizeCodexApprovalPolicy(
|
|
303
|
+
profile.approvalPolicy,
|
|
304
|
+
null
|
|
305
|
+
);
|
|
306
|
+
if (!sandboxMode && !approvalPolicy) return null;
|
|
307
|
+
return {
|
|
308
|
+
sandboxMode,
|
|
309
|
+
approvalPolicy,
|
|
310
|
+
permissionMode: sandboxMode,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeSenderId(senderId) {
|
|
315
|
+
const text = String(senderId || '').trim();
|
|
316
|
+
return text || '';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sameCodexPermissionProfile(left, right) {
|
|
320
|
+
const normalizedLeft = normalizeComparableCodexPermissionProfile(left);
|
|
321
|
+
const normalizedRight = normalizeComparableCodexPermissionProfile(right);
|
|
322
|
+
if (!normalizedLeft || !normalizedRight) return false;
|
|
323
|
+
const sameSandbox = normalizedLeft.sandboxMode === normalizedRight.sandboxMode;
|
|
324
|
+
const leftApproval = String(normalizedLeft.approvalPolicy || '').trim();
|
|
325
|
+
const rightApproval = String(normalizedRight.approvalPolicy || '').trim();
|
|
326
|
+
if (!leftApproval || !rightApproval) return sameSandbox;
|
|
327
|
+
return sameSandbox && leftApproval === rightApproval;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function codexSandboxPrivilegeRank(value) {
|
|
331
|
+
const normalized = normalizeCodexSandboxMode(value, null);
|
|
332
|
+
if (normalized === 'read-only') return 0;
|
|
333
|
+
if (normalized === 'workspace-write') return 1;
|
|
334
|
+
if (normalized === 'danger-full-access') return 2;
|
|
335
|
+
return -1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function codexApprovalPrivilegeRank(value) {
|
|
339
|
+
const normalized = normalizeCodexApprovalPolicy(value, null);
|
|
340
|
+
if (normalized === 'untrusted') return 0;
|
|
341
|
+
if (normalized === 'on-request') return 1;
|
|
342
|
+
if (normalized === 'on-failure') return 2;
|
|
343
|
+
if (normalized === 'never') return 3;
|
|
344
|
+
return -1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function codexNeedsFallbackForRequestedPermissions(actualProfile, requestedProfile) {
|
|
348
|
+
const normalizedActual = normalizeComparableCodexPermissionProfile(actualProfile);
|
|
349
|
+
const normalizedRequested = normalizeComparableCodexPermissionProfile(requestedProfile);
|
|
350
|
+
if (!normalizedActual || !normalizedRequested) return false;
|
|
351
|
+
return (
|
|
352
|
+
codexSandboxPrivilegeRank(normalizedActual.sandboxMode) < codexSandboxPrivilegeRank(normalizedRequested.sandboxMode)
|
|
353
|
+
|| codexApprovalPrivilegeRank(normalizedActual.approvalPolicy) < codexApprovalPrivilegeRank(normalizedRequested.approvalPolicy)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function buildCodexFallbackBridgePrompt({ fullPrompt, previousSessionId, previousProfile, requestedProfile, recentContext }) {
|
|
358
|
+
const bridge = [];
|
|
359
|
+
bridge.push('[Note: continuing the same MetaMe persona conversation on a fresh Codex execution thread because the previous thread could not satisfy the newly requested permission profile.]');
|
|
360
|
+
if (previousSessionId) {
|
|
361
|
+
bridge.push(`Previous Codex thread: ${String(previousSessionId).slice(0, 8)}`);
|
|
362
|
+
}
|
|
363
|
+
if (previousProfile || requestedProfile) {
|
|
364
|
+
const previousSummary = previousProfile
|
|
365
|
+
? `${previousProfile.sandboxMode || previousProfile.permissionMode || 'unknown'}/${previousProfile.approvalPolicy || 'unknown'}`
|
|
366
|
+
: 'unknown/unknown';
|
|
367
|
+
const requestedSummary = requestedProfile
|
|
368
|
+
? `${requestedProfile.sandboxMode || requestedProfile.permissionMode || 'unknown'}/${requestedProfile.approvalPolicy || 'unknown'}`
|
|
369
|
+
: 'unknown/unknown';
|
|
370
|
+
bridge.push(`Permission migration: ${previousSummary} -> ${requestedSummary}`);
|
|
371
|
+
}
|
|
372
|
+
if (recentContext && (recentContext.lastUser || recentContext.lastAssistant)) {
|
|
373
|
+
bridge.push('Recent conversation context:');
|
|
374
|
+
if (recentContext.lastUser) bridge.push(`Last user message: ${String(recentContext.lastUser).trim()}`);
|
|
375
|
+
if (recentContext.lastAssistant) bridge.push(`Last assistant reply: ${String(recentContext.lastAssistant).trim()}`);
|
|
376
|
+
}
|
|
377
|
+
bridge.push('Continue as the same conversation. Do not mention any internal thread migration unless the user explicitly asks.');
|
|
378
|
+
return `${bridge.join('\n')}\n\n[Current user message follows:]\n\n${fullPrompt}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getActualCodexPermissionProfile(session) {
|
|
382
|
+
if (!session || !session.id) return null;
|
|
383
|
+
if (typeof getCodexSessionSandboxProfile === 'function') {
|
|
384
|
+
return getCodexSessionSandboxProfile(session.id);
|
|
385
|
+
}
|
|
386
|
+
if (typeof getCodexSessionPermissionMode === 'function') {
|
|
387
|
+
const permissionMode = getCodexSessionPermissionMode(session.id);
|
|
388
|
+
return permissionMode ? { sandboxMode: permissionMode, approvalPolicy: null, permissionMode } : null;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function inspectClaudeResumeSession(session) {
|
|
394
|
+
const result = {
|
|
395
|
+
shouldResume: true,
|
|
396
|
+
modelPin: null,
|
|
397
|
+
reason: '',
|
|
398
|
+
};
|
|
399
|
+
if (!session || !session.started || !session.id) return result;
|
|
400
|
+
try {
|
|
401
|
+
const sessionFile = findSessionFile && findSessionFile(session.id);
|
|
402
|
+
if (!sessionFile) return result;
|
|
403
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
|
|
404
|
+
for (const line of lines.slice(0, 30)) {
|
|
405
|
+
const entry = JSON.parse(line);
|
|
406
|
+
const sessionModel = entry && entry.message && entry.message.model;
|
|
407
|
+
if (!sessionModel || sessionModel === '<synthetic>') continue;
|
|
408
|
+
if (!sessionModel.startsWith('claude-')) {
|
|
409
|
+
return {
|
|
410
|
+
shouldResume: false,
|
|
411
|
+
modelPin: null,
|
|
412
|
+
reason: 'non-claude-session',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
shouldResume: true,
|
|
417
|
+
modelPin: sessionModel,
|
|
418
|
+
reason: '',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isClaudeThinkingSignatureError(errMsg) {
|
|
428
|
+
const msg = String(errMsg || '');
|
|
429
|
+
return msg.includes('Invalid signature') || msg.includes('thinking block');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function formatClaudeResumeFallbackUserMessage(retryError) {
|
|
433
|
+
if (retryError) {
|
|
434
|
+
return '⚠️ 旧 session 无法继续,已自动切换到新 session,但本次请求仍失败。';
|
|
435
|
+
}
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function classifyCodexResumeFailure(error, errorCode) {
|
|
440
|
+
const message = String(error || '').trim();
|
|
441
|
+
const code = String(errorCode || '').trim();
|
|
442
|
+
const lowered = message.toLowerCase();
|
|
443
|
+
if (code === 'INTERRUPTED_USER') {
|
|
444
|
+
return {
|
|
445
|
+
kind: 'user-stop',
|
|
446
|
+
userMessage: '⚠️ 当前执行已按你的停止动作中断,本轮不会自动续跑。',
|
|
447
|
+
retryPromptPrefix: '',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const interrupted = (
|
|
451
|
+
lowered.includes('stopped by user')
|
|
452
|
+
|| lowered.includes('interrupted')
|
|
453
|
+
|| lowered.includes('signal')
|
|
454
|
+
|| code === 'INTERRUPTED'
|
|
455
|
+
|| code === 'INTERRUPTED_RESTART'
|
|
456
|
+
);
|
|
457
|
+
if (interrupted) {
|
|
458
|
+
return {
|
|
459
|
+
kind: 'interrupted',
|
|
460
|
+
userMessage: '⚠️ 后台刚刚重启或本轮执行被中断。系统正在自动恢复到同一条会话,请稍等。',
|
|
461
|
+
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:]',
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
kind: 'expired',
|
|
466
|
+
userMessage: '⚠️ Codex session 已过期,上下文可能丢失。正在以全新 session 重试,请在回复后补充必要背景。',
|
|
467
|
+
retryPromptPrefix: '[Note: previous Codex session expired and could not be resumed. Treating this as a new session. User message follows:]',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
187
471
|
|
|
188
472
|
/**
|
|
189
473
|
* Parse [[FILE:...]] markers from Claude output.
|
|
@@ -476,6 +760,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
476
760
|
timeoutMs = 600000,
|
|
477
761
|
chatId = null,
|
|
478
762
|
metameProject = '',
|
|
763
|
+
metameSenderId = '',
|
|
479
764
|
runtime = null,
|
|
480
765
|
onSession = null,
|
|
481
766
|
) {
|
|
@@ -495,7 +780,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
495
780
|
cwd,
|
|
496
781
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
497
782
|
detached: process.platform !== 'win32',
|
|
498
|
-
env: rt.buildEnv({ metameProject }),
|
|
783
|
+
env: rt.buildEnv({ metameProject, metameSenderId }),
|
|
499
784
|
});
|
|
500
785
|
log('INFO', `[TIMING:${chatId}] spawned ${rt.name} pid=${child.pid}`);
|
|
501
786
|
|
|
@@ -503,6 +788,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
503
788
|
activeProcesses.set(chatId, {
|
|
504
789
|
child,
|
|
505
790
|
aborted: false,
|
|
791
|
+
abortReason: null,
|
|
506
792
|
startedAt: _spawnAt,
|
|
507
793
|
engine: rt.name,
|
|
508
794
|
killSignal: rt.killSignal || 'SIGTERM',
|
|
@@ -530,11 +816,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
530
816
|
const now = Date.now();
|
|
531
817
|
if (!force && now - _lastStreamFlush < STREAM_THROTTLE) return;
|
|
532
818
|
_lastStreamFlush = now;
|
|
533
|
-
onStatus('__STREAM_TEXT__' + _streamText).catch(() => {});
|
|
819
|
+
onStatus('__STREAM_TEXT__' + _streamText).catch(() => { });
|
|
534
820
|
}
|
|
535
821
|
const writtenFiles = [];
|
|
536
822
|
const toolUsageLog = [];
|
|
537
823
|
|
|
824
|
+
void timeoutMs;
|
|
538
825
|
const engineTimeouts = rt.timeouts || {};
|
|
539
826
|
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs || (5 * 60 * 1000);
|
|
540
827
|
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs || (25 * 60 * 1000);
|
|
@@ -583,7 +870,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
583
870
|
if (onStatus) {
|
|
584
871
|
const milestoneMsg = parts.join(' | ');
|
|
585
872
|
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${milestoneMsg}` : milestoneMsg;
|
|
586
|
-
onStatus(msg).catch(() => {});
|
|
873
|
+
onStatus(msg).catch(() => { });
|
|
587
874
|
}
|
|
588
875
|
}
|
|
589
876
|
}, 30000);
|
|
@@ -721,7 +1008,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
721
1008
|
if (onStatus) {
|
|
722
1009
|
// Overlay tool status on top of streamed text (if any); else show plain status
|
|
723
1010
|
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${status}` : status;
|
|
724
|
-
onStatus(msg).catch(() => {});
|
|
1011
|
+
onStatus(msg).catch(() => { });
|
|
725
1012
|
}
|
|
726
1013
|
}
|
|
727
1014
|
}
|
|
@@ -755,10 +1042,21 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
755
1042
|
|
|
756
1043
|
const proc = chatId ? activeProcesses.get(chatId) : null;
|
|
757
1044
|
const wasAborted = proc && proc.aborted;
|
|
1045
|
+
const abortReason = proc && proc.abortReason ? String(proc.abortReason) : '';
|
|
758
1046
|
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
759
1047
|
|
|
760
1048
|
if (wasAborted) {
|
|
761
|
-
finalize({
|
|
1049
|
+
finalize({
|
|
1050
|
+
output: finalResult || null,
|
|
1051
|
+
error: 'Stopped by user',
|
|
1052
|
+
errorCode: (abortReason === 'daemon-restart' || abortReason === 'shutdown')
|
|
1053
|
+
? 'INTERRUPTED_RESTART'
|
|
1054
|
+
: 'INTERRUPTED_USER',
|
|
1055
|
+
files: writtenFiles,
|
|
1056
|
+
toolUsageLog,
|
|
1057
|
+
usage: finalUsage,
|
|
1058
|
+
sessionId: observedSessionId || '',
|
|
1059
|
+
});
|
|
762
1060
|
return;
|
|
763
1061
|
}
|
|
764
1062
|
if (killed) {
|
|
@@ -812,11 +1110,22 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
812
1110
|
|
|
813
1111
|
// Track outbound message_id → session for reply-based session restoration.
|
|
814
1112
|
// Keeps last 200 entries to avoid unbounded growth.
|
|
815
|
-
function trackMsgSession(messageId, session, agentKey) {
|
|
816
|
-
if (!messageId || !session
|
|
1113
|
+
function trackMsgSession(messageId, session, agentKey, options = {}) {
|
|
1114
|
+
if (!messageId || !session) return;
|
|
1115
|
+
const forceRouteOnly = !!(options && options.routeOnly);
|
|
1116
|
+
if (!forceRouteOnly && !session.id) return;
|
|
817
1117
|
const st = loadState();
|
|
818
1118
|
if (!st.msg_sessions) st.msg_sessions = {};
|
|
819
|
-
st.msg_sessions[messageId] = {
|
|
1119
|
+
st.msg_sessions[messageId] = {
|
|
1120
|
+
...(session.id && !forceRouteOnly ? { id: session.id } : {}),
|
|
1121
|
+
...(session.cwd ? { cwd: session.cwd } : {}),
|
|
1122
|
+
engine: session.engine || getDefaultEngine(),
|
|
1123
|
+
logicalChatId: session.logicalChatId || null,
|
|
1124
|
+
agentKey: agentKey || null,
|
|
1125
|
+
...(session.sandboxMode ? { sandboxMode: session.sandboxMode } : {}),
|
|
1126
|
+
...(session.approvalPolicy ? { approvalPolicy: session.approvalPolicy } : {}),
|
|
1127
|
+
...(session.permissionMode ? { permissionMode: session.permissionMode } : {}),
|
|
1128
|
+
};
|
|
820
1129
|
const keys = Object.keys(st.msg_sessions);
|
|
821
1130
|
if (keys.length > 200) {
|
|
822
1131
|
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
@@ -845,7 +1154,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
845
1154
|
return loadConfig();
|
|
846
1155
|
}
|
|
847
1156
|
|
|
848
|
-
async function askClaude(bot, chatId, prompt, config, readOnly = false) {
|
|
1157
|
+
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null) {
|
|
849
1158
|
const _t0 = Date.now();
|
|
850
1159
|
log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
|
|
851
1160
|
// Track interaction time for idle/sleep detection
|
|
@@ -864,7 +1173,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
864
1173
|
let _lastStatusCardContent = null; // tracks last clean text written to card (for final-reply dedup)
|
|
865
1174
|
// Early detect bound project for branded ack card (team members / dispatch agents)
|
|
866
1175
|
const _ackChatIdStr = String(chatId);
|
|
867
|
-
const _ackAgentMap = {
|
|
1176
|
+
const _ackAgentMap = {
|
|
1177
|
+
...(config.telegram ? config.telegram.chat_agent_map || {} : {}),
|
|
1178
|
+
...(config.feishu ? config.feishu.chat_agent_map || {} : {}),
|
|
1179
|
+
...(config.imessage ? config.imessage.chat_agent_map || {} : {}),
|
|
1180
|
+
};
|
|
868
1181
|
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
869
1182
|
const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
|
|
870
1183
|
// _ackCardHeader: non-null for agents with icon/name (team members, dispatch); passed to editMessage to preserve header on streaming edits
|
|
@@ -874,12 +1187,14 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
874
1187
|
// Fire-and-forget: don't await Telegram RTT before spawning the engine process.
|
|
875
1188
|
// statusMsgId will be populated well before the first model output (~5s for codex).
|
|
876
1189
|
// For branded agents: send a card with header so streaming edits preserve the agent identity.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1190
|
+
if (!bot.suppressAck) {
|
|
1191
|
+
const _ackFn = (_ackCardHeader && bot.sendCard)
|
|
1192
|
+
? () => bot.sendCard(chatId, { title: _ackCardHeader.title, body: '🤔', color: _ackCardHeader.color })
|
|
1193
|
+
: () => (bot.sendMarkdown ? bot.sendMarkdown(chatId, '🤔') : bot.sendMessage(chatId, '🤔'));
|
|
1194
|
+
_ackFn()
|
|
1195
|
+
.then(msg => { if (msg && msg.message_id) statusMsgId = msg.message_id; })
|
|
1196
|
+
.catch(e => log('ERROR', `Failed to send ack to ${chatId}: ${e.message}`));
|
|
1197
|
+
}
|
|
883
1198
|
bot.sendTyping(chatId).catch(() => { });
|
|
884
1199
|
const typingTimer = setInterval(() => {
|
|
885
1200
|
bot.sendTyping(chatId).catch(() => { });
|
|
@@ -890,489 +1205,580 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
890
1205
|
// kill the handler, leaving the typing indicator spinning forever.
|
|
891
1206
|
try { // ── safety-net-start ──
|
|
892
1207
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1208
|
+
// Agent nickname routing: "贾维斯" / "小美,帮我..." → switch project session
|
|
1209
|
+
// Strict chats (chat_agent_map bound groups) must NOT switch agents via nickname
|
|
1210
|
+
const _strictAgentMap = {
|
|
1211
|
+
...(config.telegram ? config.telegram.chat_agent_map : {}),
|
|
1212
|
+
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1213
|
+
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1214
|
+
};
|
|
1215
|
+
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
1216
|
+
const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
|
|
1217
|
+
if (agentMatch) {
|
|
1218
|
+
const { key, proj, rest } = agentMatch;
|
|
1219
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
1220
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine ? normalizeEngineName(proj.engine) : getDefaultEngine());
|
|
1221
|
+
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
1222
|
+
if (!rest) {
|
|
1223
|
+
// Pure nickname call — confirm switch and stop
|
|
1224
|
+
clearInterval(typingTimer);
|
|
1225
|
+
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
1226
|
+
return { ok: true };
|
|
1227
|
+
}
|
|
1228
|
+
// Nickname + content — strip nickname, continue with rest as prompt
|
|
1229
|
+
prompt = rest;
|
|
908
1230
|
}
|
|
909
|
-
// Nickname + content — strip nickname, continue with rest as prompt
|
|
910
|
-
prompt = rest;
|
|
911
|
-
}
|
|
912
1231
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
(boundProject && boundProject.
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1232
|
+
// Skill routing: detect skill first, then decide session
|
|
1233
|
+
// BUT: skip skill routing if agent addressed by nickname OR chat already has an active session
|
|
1234
|
+
// (active conversation should never be hijacked by keyword-based skill matching)
|
|
1235
|
+
const chatIdStr = String(chatId);
|
|
1236
|
+
const chatAgentMap = {
|
|
1237
|
+
...(config.telegram ? config.telegram.chat_agent_map : {}),
|
|
1238
|
+
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1239
|
+
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1240
|
+
};
|
|
1241
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
1242
|
+
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
1243
|
+
const daemonCfg = (config && config.daemon) || {};
|
|
1244
|
+
// Keep real group chats on their own session key.
|
|
1245
|
+
// Only true virtual agents (_agent_*) should use the virtual namespace.
|
|
1246
|
+
const sessionChatId = getSessionChatId(chatId, boundProjectKey);
|
|
1247
|
+
const sessionRaw = getSession(sessionChatId);
|
|
1248
|
+
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
1249
|
+
const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
|
|
1250
|
+
|
|
1251
|
+
// Engine is determined from config only — bound agent config wins, then global default.
|
|
1252
|
+
const engineName = normalizeEngineName(
|
|
1253
|
+
(boundProject && boundProject.engine) || getDefaultEngine()
|
|
1254
|
+
);
|
|
1255
|
+
const runtime = getEngineRuntime(engineName);
|
|
1256
|
+
const requestedCodexPermissionProfile = engineName === 'codex'
|
|
1257
|
+
? getCodexPermissionProfile(readOnly, daemonCfg)
|
|
1258
|
+
: null;
|
|
1259
|
+
|
|
1260
|
+
// hasActiveSession: does the current engine have an ongoing conversation?
|
|
1261
|
+
const hasActiveSession = sessionRaw && (
|
|
1262
|
+
sessionRaw.engines ? !!(sessionRaw.engines[engineName]?.started) : !!sessionRaw.started
|
|
1263
|
+
);
|
|
1264
|
+
const detectedSkill = routeSkill(prompt);
|
|
1265
|
+
const skill = shouldAutoRouteSkill({
|
|
1266
|
+
agentMatch,
|
|
1267
|
+
hasActiveSession,
|
|
1268
|
+
boundProjectKey,
|
|
1269
|
+
skillName: detectedSkill,
|
|
1270
|
+
})
|
|
1271
|
+
? detectedSkill
|
|
1272
|
+
: null;
|
|
1273
|
+
|
|
1274
|
+
if (!sessionRaw) {
|
|
1275
|
+
// No saved state for this chatId: start a fresh session.
|
|
1276
|
+
// Note: daemon_state.json persists across restarts, so this only happens on truly first use
|
|
1277
|
+
// or after an explicit /new command.
|
|
1278
|
+
createSession(
|
|
1279
|
+
sessionChatId,
|
|
1280
|
+
boundCwd || undefined,
|
|
1281
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1282
|
+
boundEngineName,
|
|
1283
|
+
boundEngineName === 'codex' ? requestedCodexPermissionProfile : undefined
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
945
1286
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1287
|
+
// Resolve flat view for current engine (id + started are engine-specific; cwd is shared)
|
|
1288
|
+
let session = resolveSessionForEngine(sessionChatId, engineName) || { cwd: boundCwd || HOME, engine: engineName, id: null, started: false };
|
|
1289
|
+
session.engine = engineName; // keep local copy for Codex resume detection below
|
|
1290
|
+
session.logicalChatId = sessionChatId;
|
|
1291
|
+
|
|
1292
|
+
// Pre-spawn session validation: unified for all engines.
|
|
1293
|
+
// Claude checks JSONL file existence; Codex checks SQLite. Same interface, different backend.
|
|
1294
|
+
// Skip warning for virtual agents (team members) - they may use worktrees with fresh sessions
|
|
1295
|
+
const isVirtualAgent = String(sessionChatId).startsWith('_agent_');
|
|
1296
|
+
if (session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
1297
|
+
const valid = validateEngineSession(engineName, session.id, session.cwd);
|
|
1298
|
+
if (!valid) {
|
|
1299
|
+
log('WARN', `${engineName} session ${session.id.slice(0, 8)} invalid for ${sessionChatId}; starting fresh ${engineName} session`);
|
|
1300
|
+
if (!isVirtualAgent) {
|
|
1301
|
+
await bot.sendMessage(chatId, '⚠️ 上次 session 已失效,已自动开启新 session。').catch(() => { });
|
|
1302
|
+
}
|
|
1303
|
+
session = createSession(
|
|
1304
|
+
sessionChatId,
|
|
1305
|
+
session.cwd,
|
|
1306
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1307
|
+
engineName,
|
|
1308
|
+
engineName === 'codex' ? requestedCodexPermissionProfile : undefined
|
|
1309
|
+
);
|
|
960
1310
|
}
|
|
961
|
-
session = createSession(sessionChatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', engineName);
|
|
962
1311
|
}
|
|
963
|
-
}
|
|
964
1312
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1313
|
+
if (runtime.name === 'codex' && session.started && session.id) {
|
|
1314
|
+
const actualPermissionProfile = getActualCodexPermissionProfile(session);
|
|
1315
|
+
if (actualPermissionProfile) {
|
|
1316
|
+
const storedPermissionProfile = normalizeComparableCodexPermissionProfile(session);
|
|
1317
|
+
if (!sameCodexPermissionProfile(storedPermissionProfile, actualPermissionProfile)) {
|
|
1318
|
+
session = { ...session, ...actualPermissionProfile };
|
|
1319
|
+
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1320
|
+
const engines = { ...(cur.engines || {}) };
|
|
1321
|
+
engines.codex = {
|
|
1322
|
+
...(engines.codex || {}),
|
|
1323
|
+
...(actualPermissionProfile || {}),
|
|
1324
|
+
};
|
|
1325
|
+
return { ...cur, engines };
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
if (!sameCodexPermissionProfile(actualPermissionProfile, requestedCodexPermissionProfile)) {
|
|
1329
|
+
const actualSummary = `${actualPermissionProfile.sandboxMode || actualPermissionProfile.permissionMode || 'unknown'}/${actualPermissionProfile.approvalPolicy || 'unknown'}`;
|
|
1330
|
+
const requestedSummary = `${requestedCodexPermissionProfile.sandboxMode}/${requestedCodexPermissionProfile.approvalPolicy}`;
|
|
1331
|
+
log('INFO', `Codex session ${session.id.slice(0, 8)} permission differs for ${sessionChatId}: ${actualSummary} vs requested ${requestedSummary}; preserving existing session continuity`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const mentorCfg = (daemonCfg.mentor && typeof daemonCfg.mentor === 'object') ? daemonCfg.mentor : {};
|
|
1336
|
+
const mentorEnabled = !!(mentorEngine && mentorCfg.enabled);
|
|
1337
|
+
const excludeAgents = new Set(
|
|
1338
|
+
(Array.isArray(mentorCfg.exclude_agents) ? mentorCfg.exclude_agents : [])
|
|
1339
|
+
.map(x => String(x || '').trim())
|
|
1340
|
+
.filter(Boolean)
|
|
1341
|
+
);
|
|
1342
|
+
const chatAgentKey = boundProjectKey || 'personal';
|
|
1343
|
+
const mentorExcluded = excludeAgents.has(chatAgentKey);
|
|
1344
|
+
let mentorSuppressed = false;
|
|
976
1345
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1346
|
+
// Mentor pre-flight breaker: first hit sends a short reassurance; cooldown does not block normal answers.
|
|
1347
|
+
if (mentorEnabled && !mentorExcluded) {
|
|
1348
|
+
try {
|
|
1349
|
+
const breaker = mentorEngine.checkEmotionBreaker(prompt, mentorCfg);
|
|
1350
|
+
if (breaker && breaker.tripped) {
|
|
1351
|
+
mentorSuppressed = true;
|
|
1352
|
+
if (breaker.reason !== 'cooldown_active' && breaker.response) {
|
|
1353
|
+
await bot.sendMessage(chatId, breaker.response).catch(() => { });
|
|
1354
|
+
}
|
|
985
1355
|
}
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
log('WARN', `Mentor breaker failed: ${e.message}`);
|
|
986
1358
|
}
|
|
987
|
-
} catch (e) {
|
|
988
|
-
log('WARN', `Mentor breaker failed: ${e.message}`);
|
|
989
1359
|
}
|
|
990
|
-
}
|
|
991
1360
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
model
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
session
|
|
999
|
-
|
|
1000
|
-
|
|
1361
|
+
// Build engine command — prefer per-engine model, fall back to legacy daemon.model
|
|
1362
|
+
let model = resolveEngineModel(runtime.name, daemonCfg, boundProject && boundProject.model);
|
|
1363
|
+
|
|
1364
|
+
// When resuming a Claude session, inspect the original model first.
|
|
1365
|
+
// Thinking block signatures are model-specific; non-Claude JSONL sessions
|
|
1366
|
+
// must not be resumed as Claude.
|
|
1367
|
+
if (runtime.name === 'claude' && session.started && session.id) {
|
|
1368
|
+
const resumeInspection = inspectClaudeResumeSession(session);
|
|
1369
|
+
if (resumeInspection.shouldResume === false) {
|
|
1370
|
+
log('INFO', `[ModelPin] session ${session.id.slice(0, 8)} flagged as ${resumeInspection.reason}; starting fresh Claude session`);
|
|
1371
|
+
session = createSession(sessionChatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', runtime.name);
|
|
1372
|
+
} else if (resumeInspection.modelPin) {
|
|
1373
|
+
if (resumeInspection.modelPin !== model) {
|
|
1374
|
+
log('INFO', `[ModelPin] resuming ${session.id.slice(0, 8)} with original model ${resumeInspection.modelPin} (configured: ${model})`);
|
|
1375
|
+
}
|
|
1376
|
+
model = resumeInspection.modelPin;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1001
1379
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (soulContent) parts.push(soulContent);
|
|
1380
|
+
let agentHint = '';
|
|
1381
|
+
if (!session.started && (boundProject || (session && session.cwd))) {
|
|
1382
|
+
try {
|
|
1383
|
+
// Engine-aware: Codex gets memory only (soul is already in AGENTS.md);
|
|
1384
|
+
// Claude gets soul + memory (SOUL.md is not auto-loaded by Claude).
|
|
1385
|
+
agentHint = buildAgentContextForEngine(
|
|
1386
|
+
boundProject || { cwd: session.cwd },
|
|
1387
|
+
engineName,
|
|
1388
|
+
HOME,
|
|
1389
|
+
).hint || '';
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
log('WARN', `Agent context injection failed: ${e.message}`);
|
|
1015
1392
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Memory & Knowledge Injection (RAG)
|
|
1396
|
+
let memoryHint = '';
|
|
1397
|
+
|
|
1398
|
+
// Compact context injection: injected once on first message after /compact, then cleared
|
|
1399
|
+
if (!session.started && session.compactContext) {
|
|
1400
|
+
const _compactCtx = String(session.compactContext).trim();
|
|
1401
|
+
if (_compactCtx) {
|
|
1402
|
+
memoryHint += `\n\n[Context from previous session (compacted):\n${_compactCtx}]`;
|
|
1403
|
+
try {
|
|
1404
|
+
const _stC = loadState();
|
|
1405
|
+
const _engSlot = _stC.sessions && _stC.sessions[sessionChatId] && _stC.sessions[sessionChatId].engines
|
|
1406
|
+
? _stC.sessions[sessionChatId].engines[engineName]
|
|
1407
|
+
: null;
|
|
1408
|
+
if (_engSlot) { delete _engSlot.compactContext; saveState(_stC); }
|
|
1409
|
+
} catch { /* non-critical */ }
|
|
1019
1410
|
}
|
|
1020
|
-
} catch (e) {
|
|
1021
|
-
log('WARN', `AGENTS.md refresh failed: ${e.message}`);
|
|
1022
1411
|
}
|
|
1023
|
-
}
|
|
1024
1412
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1413
|
+
// projectKey must be declared outside the try block so the daemonHint template below can reference it.
|
|
1414
|
+
const _cid0 = String(chatId);
|
|
1415
|
+
const _agentMap0 = {
|
|
1416
|
+
...(config.telegram ? config.telegram.chat_agent_map : {}),
|
|
1417
|
+
...(config.feishu ? config.feishu.chat_agent_map : {}),
|
|
1418
|
+
...(config.imessage ? config.imessage.chat_agent_map : {}),
|
|
1419
|
+
};
|
|
1420
|
+
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1027
1421
|
try {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1422
|
+
const memory = require('./memory');
|
|
1423
|
+
|
|
1424
|
+
// L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
|
|
1425
|
+
if (!session.started) {
|
|
1426
|
+
try {
|
|
1427
|
+
const nowDir = path.join(HOME, '.metame', 'memory', 'now');
|
|
1428
|
+
const nowKey = projectKey || 'default';
|
|
1429
|
+
const nowPath = path.join(nowDir, `${nowKey}.md`);
|
|
1430
|
+
if (fs.existsSync(nowPath)) {
|
|
1431
|
+
const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
|
|
1432
|
+
if (nowContent) {
|
|
1433
|
+
memoryHint += `\n\n[Current task context:\n${nowContent}]`;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
} catch { /* non-critical */ }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// 1. Inject recent session memories ONLY on first message of a session
|
|
1440
|
+
if (!session.started) {
|
|
1441
|
+
const recent = memory.recentSessions({ limit: 1, project: projectKey || undefined });
|
|
1442
|
+
if (recent.length > 0) {
|
|
1443
|
+
const items = recent.map(r => `- [${r.created_at}] ${r.summary}${r.keywords ? ' (keywords: ' + r.keywords + ')' : ''}`).join('\n');
|
|
1444
|
+
memoryHint += `\n\n[Past session memory:\n${items}]`;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// 2. Dynamic Fact Injection (RAG) — first message only
|
|
1449
|
+
// Facts stay in Claude's context for the rest of the session; no need to repeat.
|
|
1450
|
+
// Uses QMD hybrid search if available, falls back to FTS5.
|
|
1451
|
+
if (!session.started) {
|
|
1452
|
+
const searchFn = memory.searchFactsAsync || memory.searchFacts;
|
|
1453
|
+
const factQuery = buildFactSearchQuery(prompt, projectKey);
|
|
1454
|
+
const facts = await Promise.resolve(searchFn(factQuery, { limit: 3, project: projectKey || undefined }));
|
|
1455
|
+
if (facts.length > 0) {
|
|
1456
|
+
// Separate capsule facts from regular facts
|
|
1457
|
+
const capsuleFacts = facts.filter(f => f.relation === 'knowledge_capsule');
|
|
1458
|
+
const regularFacts = facts.filter(f => f.relation !== 'knowledge_capsule');
|
|
1459
|
+
|
|
1460
|
+
// Inject regular facts as before
|
|
1461
|
+
if (regularFacts.length > 0) {
|
|
1462
|
+
const factItems = regularFacts.map(f => `- [${f.relation}] ${f.value}`).join('\n');
|
|
1463
|
+
memoryHint += `\n\n[Relevant facts:\n${factItems}]`;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Capsule facts: derive file path from entity and inject as direct "must read" hint
|
|
1467
|
+
// Entity pattern: capsule.metame_daemon_dispatch → capsules/metame-daemon-dispatch-playbook.md
|
|
1468
|
+
if (capsuleFacts.length > 0) {
|
|
1469
|
+
const capsulePaths = capsuleFacts.map(f => {
|
|
1470
|
+
const slug = f.entity.replace(/^capsule\./, '').replace(/_/g, '-');
|
|
1471
|
+
return path.join(HOME, '.metame', 'memory', 'capsules', `${slug}-playbook.md`);
|
|
1472
|
+
}).filter(p => fs.existsSync(p));
|
|
1473
|
+
if (capsulePaths.length > 0) {
|
|
1474
|
+
// Inject file paths only (no shell commands) — works cross-platform and with all engines.
|
|
1475
|
+
// Claude Code reads via Read tool; Codex/Gemini parse the path directly.
|
|
1476
|
+
memoryHint += `\n\n[Relevant playbook detected — read before answering:\n${capsulePaths.map(p => ` ${p}`).join('\n')}]`;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
log('INFO', `[MEMORY] Injected ${regularFacts.length} facts, ${capsuleFacts.length} capsule(s) (query_len=${factQuery.length})`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
memory.close();
|
|
1035
1485
|
} catch (e) {
|
|
1036
|
-
log('WARN', `
|
|
1486
|
+
if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
|
|
1037
1487
|
}
|
|
1038
|
-
}
|
|
1039
1488
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const _cid0 = String(chatId);
|
|
1044
|
-
const _agentMap0 = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
1045
|
-
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1046
|
-
try {
|
|
1047
|
-
const memory = require('./memory');
|
|
1048
|
-
|
|
1049
|
-
// L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
|
|
1489
|
+
// ZPD: build competence hint from brain profile
|
|
1490
|
+
let zdpHint = '';
|
|
1491
|
+
let brainDoc = null;
|
|
1050
1492
|
if (!session.started) {
|
|
1051
1493
|
try {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
if (
|
|
1058
|
-
|
|
1494
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1495
|
+
if (fs.existsSync(brainPath)) {
|
|
1496
|
+
const brain = yaml.load(fs.readFileSync(brainPath, 'utf8'));
|
|
1497
|
+
brainDoc = brain;
|
|
1498
|
+
const cmap = brain && brain.user_competence_map;
|
|
1499
|
+
if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
|
|
1500
|
+
const lines = Object.entries(cmap)
|
|
1501
|
+
.map(([domain, level]) => ` ${domain}: ${level}`)
|
|
1502
|
+
.join('\n');
|
|
1503
|
+
zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
|
|
1059
1504
|
}
|
|
1060
1505
|
}
|
|
1061
1506
|
} catch { /* non-critical */ }
|
|
1062
1507
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const items = recent.map(r => `- [${r.created_at}] ${r.summary}${r.keywords ? ' (keywords: ' + r.keywords + ')' : ''}`).join('\n');
|
|
1069
|
-
memoryHint += `\n\n[Past session memory:\n${items}]`;
|
|
1070
|
-
}
|
|
1508
|
+
if (!brainDoc) {
|
|
1509
|
+
try {
|
|
1510
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1511
|
+
if (fs.existsSync(brainPath)) brainDoc = yaml.load(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1512
|
+
} catch { /* ignore */ }
|
|
1071
1513
|
}
|
|
1072
1514
|
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1515
|
+
// Inject daemon hints only on first message of a session
|
|
1516
|
+
// Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
1517
|
+
let daemonHint = '';
|
|
1076
1518
|
if (!session.started) {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
memoryHint += `\n\n[Relevant facts:\n${factItems}]`;
|
|
1083
|
-
log('INFO', `[MEMORY] Injected ${facts.length} facts (query_len=${factQuery.length})`);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
memory.close();
|
|
1088
|
-
} catch (e) {
|
|
1089
|
-
if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// ZPD: build competence hint from brain profile
|
|
1093
|
-
let zdpHint = '';
|
|
1094
|
-
let brainDoc = null;
|
|
1095
|
-
if (!session.started) {
|
|
1096
|
-
try {
|
|
1097
|
-
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1098
|
-
if (fs.existsSync(brainPath)) {
|
|
1099
|
-
const brain = yaml.load(fs.readFileSync(brainPath, 'utf8'));
|
|
1100
|
-
brainDoc = brain;
|
|
1101
|
-
const cmap = brain && brain.user_competence_map;
|
|
1102
|
-
if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
|
|
1103
|
-
const lines = Object.entries(cmap)
|
|
1104
|
-
.map(([domain, level]) => ` ${domain}: ${level}`)
|
|
1105
|
-
.join('\n');
|
|
1106
|
-
zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
} catch { /* non-critical */ }
|
|
1110
|
-
}
|
|
1111
|
-
if (!brainDoc) {
|
|
1112
|
-
try {
|
|
1113
|
-
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1114
|
-
if (fs.existsSync(brainPath)) brainDoc = yaml.load(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1115
|
-
} catch { /* ignore */ }
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// Inject daemon hints only on first message of a session
|
|
1119
|
-
// Task-specific rules (3-5) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
1120
|
-
let daemonHint = '';
|
|
1121
|
-
if (!session.started) {
|
|
1122
|
-
const taskRules = isTaskIntent(prompt) ? `
|
|
1123
|
-
3. Knowledge retrieval: When you need context about a specific topic, past decisions, or lessons, call:
|
|
1124
|
-
node ~/.metame/memory-search.js "关键词1" "keyword2"
|
|
1125
|
-
Also read ~/.metame/memory/INDEX.md to discover available long-form lesson/decision docs, then read specific files as needed.
|
|
1126
|
-
Use these before answering complex questions about MetaMe architecture or past decisions.
|
|
1127
|
-
4. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
1519
|
+
const mentorRadarHint = (config && config.daemon && config.daemon.mentor && config.daemon.mentor.enabled)
|
|
1520
|
+
? '\n When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"'
|
|
1521
|
+
: '';
|
|
1522
|
+
const taskRules = isTaskIntent(prompt) ? `
|
|
1523
|
+
3. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
1128
1524
|
node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
|
|
1129
1525
|
Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
|
|
1130
1526
|
Only write verified facts. Do not write speculative or process-description entries.
|
|
1131
|
-
|
|
1132
|
-
|
|
1527
|
+
${mentorRadarHint}
|
|
1528
|
+
4. 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:
|
|
1133
1529
|
\`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
|
|
1134
1530
|
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
1135
|
-
|
|
1531
|
+
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
1136
1532
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
1137
|
-
2.
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
1150
|
-
// without forcing users to write slash commands by hand.
|
|
1151
|
-
let macAutomationHint = '';
|
|
1152
|
-
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
1153
|
-
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
1533
|
+
2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${taskRules}]`;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
|
|
1537
|
+
|
|
1538
|
+
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
1539
|
+
|
|
1540
|
+
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
1541
|
+
// without forcing users to write slash commands by hand.
|
|
1542
|
+
let macAutomationHint = '';
|
|
1543
|
+
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
1544
|
+
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
1154
1545
|
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
1155
1546
|
2. Read/query actions can execute directly.
|
|
1156
1547
|
3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
|
|
1157
1548
|
4. Keep output concise: success/failure + key result only.
|
|
1158
1549
|
5. If permission is missing, guide user to run /mac perms open then retry.
|
|
1159
1550
|
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).]`;
|
|
1160
|
-
|
|
1551
|
+
}
|
|
1161
1552
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1553
|
+
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
1554
|
+
let summaryHint = '';
|
|
1555
|
+
if (session.started) {
|
|
1556
|
+
try {
|
|
1557
|
+
const _stSum = loadState();
|
|
1558
|
+
const _sess = _stSum.sessions && _stSum.sessions[chatId];
|
|
1559
|
+
if (_sess && _sess.last_summary && _sess.last_summary_at) {
|
|
1560
|
+
const _idleMs = Date.now() - (_sess.last_active || 0);
|
|
1561
|
+
const _summaryAgeH = (Date.now() - _sess.last_summary_at) / 3600000;
|
|
1562
|
+
if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
|
|
1563
|
+
summaryHint = `
|
|
1173
1564
|
|
|
1174
1565
|
[上次对话摘要,供参考]: ${_sess.last_summary}`;
|
|
1175
|
-
|
|
1566
|
+
log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
|
|
1567
|
+
}
|
|
1176
1568
|
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1569
|
+
} catch { /* non-critical */ }
|
|
1570
|
+
}
|
|
1180
1571
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1572
|
+
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1573
|
+
let mentorHint = '';
|
|
1574
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
1575
|
+
try {
|
|
1576
|
+
const signals = collectRecentSessionSignals(session.id, 6);
|
|
1577
|
+
let skeleton = null;
|
|
1578
|
+
if (sessionAnalytics && typeof sessionAnalytics.extractSkeleton === 'function') {
|
|
1579
|
+
const file = findSessionFile(session.id);
|
|
1580
|
+
if (file && fs.existsSync(file)) {
|
|
1581
|
+
const st = fs.statSync(file);
|
|
1582
|
+
if (st.size <= 2 * 1024 * 1024) {
|
|
1583
|
+
skeleton = sessionAnalytics.extractSkeleton(file);
|
|
1584
|
+
}
|
|
1193
1585
|
}
|
|
1194
1586
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1587
|
+
const zone = skeleton && mentorEngine.computeZone
|
|
1588
|
+
? mentorEngine.computeZone(skeleton).zone
|
|
1589
|
+
: 'stretch';
|
|
1590
|
+
const sessionState = {
|
|
1591
|
+
zone,
|
|
1592
|
+
recentMessages: signals.recentMessages,
|
|
1593
|
+
cwd: session.cwd,
|
|
1594
|
+
skeleton,
|
|
1595
|
+
sessionStartTime: signals.sessionStartTime || new Date().toISOString(),
|
|
1596
|
+
topic: String(prompt || '').slice(0, 120),
|
|
1597
|
+
currentTopic: String(prompt || '').slice(0, 120),
|
|
1598
|
+
lastUserMessage: String(prompt || '').slice(0, 200),
|
|
1599
|
+
};
|
|
1600
|
+
const built = mentorEngine.buildMentorPrompt(sessionState, brainDoc || {}, mentorCfg);
|
|
1601
|
+
if (built && String(built).trim()) mentorHint = `\n\n${String(built).trim()}`;
|
|
1602
|
+
|
|
1603
|
+
// Collect reflection debt: if user returns to same project+topic, inject recall prompt.
|
|
1604
|
+
// Suppressed by quiet_until (user explicitly asked for silence), but NOT by expert skip
|
|
1605
|
+
// (even experts may not have reviewed AI-generated code).
|
|
1606
|
+
const quietUntil = brainDoc && brainDoc.growth ? brainDoc.growth.quiet_until : null;
|
|
1607
|
+
const quietMs = quietUntil ? new Date(quietUntil).getTime() : 0;
|
|
1608
|
+
const isQuiet = quietMs && quietMs > Date.now();
|
|
1609
|
+
if (!isQuiet && mentorEngine.collectDebt) {
|
|
1610
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1611
|
+
const projectId = info && info.project_id ? info.project_id : '';
|
|
1612
|
+
if (projectId) {
|
|
1613
|
+
const debt = mentorEngine.collectDebt(projectId, String(prompt || '').slice(0, 120));
|
|
1614
|
+
if (debt && debt.prompt) {
|
|
1615
|
+
mentorHint += `\n\n[Reflection debt] ${debt.prompt}`;
|
|
1616
|
+
}
|
|
1225
1617
|
}
|
|
1226
1618
|
}
|
|
1619
|
+
} catch (e) {
|
|
1620
|
+
log('WARN', `Mentor prompt build failed: ${e.message}`);
|
|
1227
1621
|
}
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
log('WARN', `Mentor prompt build failed: ${e.message}`);
|
|
1230
1622
|
}
|
|
1231
|
-
}
|
|
1232
1623
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
(gitCheckpointAsync || gitCheckpoint)(session.cwd, prompt).catch?.(() => {});
|
|
1247
|
-
}
|
|
1248
|
-
log('INFO', `[TIMING:${chatId}] pre-spawn +${Date.now() - _t0}ms (engine:${runtime.name} started:${session.started})`);
|
|
1249
|
-
|
|
1250
|
-
// Use streaming mode to show progress
|
|
1251
|
-
// Telegram: edit status msg in-place; Feishu: edit or fallback to new messages
|
|
1252
|
-
let editFailed = false;
|
|
1253
|
-
let lastFallbackStatus = 0;
|
|
1254
|
-
const FALLBACK_THROTTLE = fallbackThrottleMs;
|
|
1255
|
-
const onStatus = async (status) => {
|
|
1256
|
-
try {
|
|
1257
|
-
if (typeof status !== 'string') return;
|
|
1258
|
-
|
|
1259
|
-
// __STREAM_TEXT__: streamed model text — edit card and track for final dedup
|
|
1260
|
-
if (status.startsWith('__STREAM_TEXT__')) {
|
|
1261
|
-
const content = status.slice('__STREAM_TEXT__'.length);
|
|
1262
|
-
// Set synchronously BEFORE await — this is the critical race fix.
|
|
1263
|
-
// flushStream(true) is called from the 'done' event (before process close),
|
|
1264
|
-
// so by setting here synchronously, _lastStatusCardContent is guaranteed to be
|
|
1265
|
-
// set before the child 'close' event fires and finalize() resolves.
|
|
1266
|
-
_lastStatusCardContent = content;
|
|
1267
|
-
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1268
|
-
const ok = await bot.editMessage(chatId, statusMsgId, content, _ackCardHeader);
|
|
1269
|
-
if (ok === false) editFailed = true;
|
|
1270
|
-
}
|
|
1271
|
-
return; // skip fallback — final reply logic will use existing card
|
|
1624
|
+
// Language guard: only inject on first message of a new session to avoid
|
|
1625
|
+
// linearly growing token cost on every turn in long conversations.
|
|
1626
|
+
// Claude Code preserves session context, so the guard persists after initial injection.
|
|
1627
|
+
const langGuard = session.started
|
|
1628
|
+
? ''
|
|
1629
|
+
: '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
1630
|
+
let intentHint = '';
|
|
1631
|
+
if (runtime.name === 'codex') {
|
|
1632
|
+
try {
|
|
1633
|
+
const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
|
|
1634
|
+
if (block) intentHint = `\n\n${block}`;
|
|
1635
|
+
} catch (e) {
|
|
1636
|
+
log('WARN', `Intent registry injection failed: ${e.message}`);
|
|
1272
1637
|
}
|
|
1638
|
+
}
|
|
1639
|
+
const fullPrompt = routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
1640
|
+
if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
|
|
1641
|
+
const actualPermissionProfile = getActualCodexPermissionProfile(session);
|
|
1642
|
+
if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
|
|
1643
|
+
const actualSummary = actualPermissionProfile
|
|
1644
|
+
? `${actualPermissionProfile.sandboxMode || actualPermissionProfile.permissionMode || 'unknown'}/${actualPermissionProfile.approvalPolicy || 'unknown'}`
|
|
1645
|
+
: 'unknown/unknown';
|
|
1646
|
+
const requestedSummary = `${requestedCodexPermissionProfile.sandboxMode}/${requestedCodexPermissionProfile.approvalPolicy}`;
|
|
1647
|
+
log('INFO', `Codex session ${session.id.slice(0, 8)} is below requested permissions for ${sessionChatId}: ${actualSummary} vs ${requestedSummary}; trying native resume first`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1273
1650
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1651
|
+
const args = runtime.buildArgs({
|
|
1652
|
+
model,
|
|
1653
|
+
readOnly,
|
|
1654
|
+
daemonCfg,
|
|
1655
|
+
session,
|
|
1656
|
+
cwd: session.cwd,
|
|
1657
|
+
permissionProfile: runtime.name === 'codex' ? requestedCodexPermissionProfile : null,
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// Codex: write/refresh AGENTS.md = CLAUDE.md + SOUL.md on every fresh execution thread.
|
|
1661
|
+
// This must happen after any permission-triggered fallback decision so the spawned process uses
|
|
1662
|
+
// the final session object and fresh exec args rather than stale resume args.
|
|
1663
|
+
if (engineName === 'codex' && session.cwd && !session.started) {
|
|
1664
|
+
try {
|
|
1665
|
+
const parts = [];
|
|
1666
|
+
const claudeMd = path.join(session.cwd, 'CLAUDE.md');
|
|
1667
|
+
const soulMd = path.join(session.cwd, 'SOUL.md');
|
|
1668
|
+
if (fs.existsSync(claudeMd)) parts.push(fs.readFileSync(claudeMd, 'utf8').trim());
|
|
1669
|
+
if (fs.existsSync(soulMd)) {
|
|
1670
|
+
const soulContent = fs.readFileSync(soulMd, 'utf8').trim();
|
|
1671
|
+
if (soulContent) parts.push(soulContent);
|
|
1280
1672
|
}
|
|
1281
|
-
|
|
1673
|
+
if (parts.length > 0) {
|
|
1674
|
+
fs.writeFileSync(path.join(session.cwd, 'AGENTS.md'), parts.join('\n\n'), 'utf8');
|
|
1675
|
+
log('INFO', `Refreshed AGENTS.md (${parts.length} section(s)) in ${session.cwd}`);
|
|
1676
|
+
}
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
log('WARN', `AGENTS.md refresh failed: ${e.message}`);
|
|
1282
1679
|
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Git checkpoint before Claude modifies files (for /undo).
|
|
1683
|
+
// Skip for virtual agents (team clones like _agent_yi) — each has its own worktree,
|
|
1684
|
+
// but checkpoint uses `git add -A` which could interfere with parallel work.
|
|
1685
|
+
const _isVirtualAgent = String(chatId).startsWith('_agent_') || String(chatId).startsWith('_scope_');
|
|
1686
|
+
if (!_isVirtualAgent) {
|
|
1687
|
+
try {
|
|
1688
|
+
const checkpointResult = (gitCheckpointAsync || gitCheckpoint)(session.cwd, prompt);
|
|
1689
|
+
if (checkpointResult && typeof checkpointResult.catch === 'function') {
|
|
1690
|
+
checkpointResult.catch(() => { });
|
|
1691
|
+
}
|
|
1692
|
+
} catch { /* non-critical */ }
|
|
1693
|
+
}
|
|
1694
|
+
log('INFO', `[TIMING:${chatId}] pre-spawn +${Date.now() - _t0}ms (engine:${runtime.name} started:${session.started})`);
|
|
1695
|
+
|
|
1696
|
+
// Use streaming mode to show progress
|
|
1697
|
+
// Telegram: edit status msg in-place; Feishu: edit or fallback to new messages
|
|
1698
|
+
let editFailed = false;
|
|
1699
|
+
let lastFallbackStatus = 0;
|
|
1700
|
+
const FALLBACK_THROTTLE = fallbackThrottleMs;
|
|
1701
|
+
const onStatus = async (status) => {
|
|
1702
|
+
try {
|
|
1703
|
+
if (typeof status !== 'string') return;
|
|
1704
|
+
|
|
1705
|
+
// __STREAM_TEXT__: streamed model text — edit card and track for final dedup
|
|
1706
|
+
if (status.startsWith('__STREAM_TEXT__')) {
|
|
1707
|
+
const content = status.slice('__STREAM_TEXT__'.length);
|
|
1708
|
+
// Set synchronously BEFORE await — this is the critical race fix.
|
|
1709
|
+
// flushStream(true) is called from the 'done' event (before process close),
|
|
1710
|
+
// so by setting here synchronously, _lastStatusCardContent is guaranteed to be
|
|
1711
|
+
// set before the child 'close' event fires and finalize() resolves.
|
|
1712
|
+
_lastStatusCardContent = content;
|
|
1713
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1714
|
+
const ok = await bot.editMessage(chatId, statusMsgId, content, _ackCardHeader);
|
|
1715
|
+
if (ok === false) editFailed = true;
|
|
1716
|
+
}
|
|
1717
|
+
return; // skip fallback — final reply logic will use existing card
|
|
1718
|
+
}
|
|
1283
1719
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1720
|
+
// __TOOL_OVERLAY__: text + tool status line — edit card but don't update _lastStatusCardContent
|
|
1721
|
+
if (status.startsWith('__TOOL_OVERLAY__')) {
|
|
1722
|
+
const content = status.slice('__TOOL_OVERLAY__'.length);
|
|
1723
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1724
|
+
await bot.editMessage(chatId, statusMsgId, content, _ackCardHeader);
|
|
1725
|
+
// intentionally NOT updating _lastStatusCardContent — overlay is transient
|
|
1726
|
+
}
|
|
1289
1727
|
return;
|
|
1290
1728
|
}
|
|
1291
|
-
editFailed = true;
|
|
1292
|
-
}
|
|
1293
|
-
// Fallback: send as new message with throttle to avoid spam
|
|
1294
|
-
const now = Date.now();
|
|
1295
|
-
if (now - lastFallbackStatus < FALLBACK_THROTTLE) return;
|
|
1296
|
-
lastFallbackStatus = now;
|
|
1297
|
-
await bot.sendMessage(chatId, status);
|
|
1298
|
-
} catch { /* ignore status update failures */ }
|
|
1299
|
-
};
|
|
1300
1729
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1730
|
+
// Plain status (tool names before any text, milestone timers, etc.)
|
|
1731
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1732
|
+
const ok = await bot.editMessage(chatId, statusMsgId, status, _ackCardHeader);
|
|
1733
|
+
if (ok !== false) {
|
|
1734
|
+
_lastStatusCardContent = status;
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
editFailed = true;
|
|
1738
|
+
}
|
|
1739
|
+
// Fallback: send as new message with throttle to avoid spam
|
|
1740
|
+
const now = Date.now();
|
|
1741
|
+
if (now - lastFallbackStatus < FALLBACK_THROTTLE) return;
|
|
1742
|
+
lastFallbackStatus = now;
|
|
1743
|
+
await bot.sendMessage(chatId, status);
|
|
1744
|
+
} catch { /* ignore status update failures */ }
|
|
1313
1745
|
};
|
|
1314
|
-
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1315
|
-
const engines = { ...(cur.engines || {}) };
|
|
1316
|
-
engines[runtime.name] = { ...(engines[runtime.name] || {}), id: safeNextId, started: true };
|
|
1317
|
-
return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
|
|
1318
|
-
});
|
|
1319
|
-
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
1320
|
-
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1321
|
-
}
|
|
1322
|
-
};
|
|
1323
1746
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
({
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1352
|
-
output,
|
|
1353
|
-
error,
|
|
1354
|
-
errorCode,
|
|
1355
|
-
canRetry: canRetryCodexResume(chatId),
|
|
1356
|
-
})) {
|
|
1357
|
-
markCodexResumeRetried(chatId);
|
|
1358
|
-
log('WARN', `Codex resume failed for ${chatId}, retrying once with fresh exec: ${String(error).slice(0, 120)}`);
|
|
1359
|
-
// Notify user explicitly — silent context loss is worse than a visible warning.
|
|
1360
|
-
await bot.sendMessage(chatId, '⚠️ Codex session 已过期,上下文丢失。正在以全新 session 重试,请在回复后补充必要背景。').catch(() => {});
|
|
1361
|
-
session = createSession(
|
|
1362
|
-
sessionChatId,
|
|
1363
|
-
session.cwd,
|
|
1364
|
-
boundProject && boundProject.name ? boundProject.name : '',
|
|
1365
|
-
'codex'
|
|
1366
|
-
);
|
|
1367
|
-
const retryArgs = runtime.buildArgs({
|
|
1368
|
-
model,
|
|
1369
|
-
readOnly,
|
|
1370
|
-
daemonCfg,
|
|
1371
|
-
session,
|
|
1372
|
-
cwd: session.cwd,
|
|
1747
|
+
const wasCodexResumeAttempt = runtime.name === 'codex'
|
|
1748
|
+
&& !!(session && session.started && session.id && session.id !== '__continue__');
|
|
1749
|
+
const onSession = async (nextSessionId) => {
|
|
1750
|
+
const safeNextId = String(nextSessionId || '').trim();
|
|
1751
|
+
if (!safeNextId) return;
|
|
1752
|
+
const prevSessionId = session && session.id ? String(session.id) : '';
|
|
1753
|
+
const wasStarted = !!(session && session.started);
|
|
1754
|
+
session = {
|
|
1755
|
+
...session,
|
|
1756
|
+
id: safeNextId,
|
|
1757
|
+
engine: runtime.name,
|
|
1758
|
+
logicalChatId: sessionChatId,
|
|
1759
|
+
started: true,
|
|
1760
|
+
};
|
|
1761
|
+
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1762
|
+
const engines = { ...(cur.engines || {}) };
|
|
1763
|
+
const actualPermissionProfile = runtime.name === 'codex'
|
|
1764
|
+
? (getActualCodexPermissionProfile({ id: safeNextId }) || requestedCodexPermissionProfile)
|
|
1765
|
+
: null;
|
|
1766
|
+
engines[runtime.name] = {
|
|
1767
|
+
...(engines[runtime.name] || {}),
|
|
1768
|
+
id: safeNextId,
|
|
1769
|
+
started: true,
|
|
1770
|
+
...(runtime.name === 'codex' ? { runtimeSessionObserved: true } : {}),
|
|
1771
|
+
...(runtime.name === 'codex' ? actualPermissionProfile : {}),
|
|
1772
|
+
};
|
|
1773
|
+
return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
|
|
1373
1774
|
});
|
|
1374
|
-
|
|
1375
|
-
|
|
1775
|
+
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
1776
|
+
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
let output, error, errorCode, files, toolUsageLog, timedOut, sessionId;
|
|
1781
|
+
try {
|
|
1376
1782
|
({
|
|
1377
1783
|
output,
|
|
1378
1784
|
error,
|
|
@@ -1380,291 +1786,464 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1380
1786
|
timedOut,
|
|
1381
1787
|
files,
|
|
1382
1788
|
toolUsageLog,
|
|
1383
|
-
usage,
|
|
1384
1789
|
sessionId,
|
|
1385
1790
|
} = await spawnClaudeStreaming(
|
|
1386
|
-
|
|
1387
|
-
|
|
1791
|
+
args,
|
|
1792
|
+
fullPrompt,
|
|
1388
1793
|
session.cwd,
|
|
1389
1794
|
onStatus,
|
|
1390
1795
|
600000,
|
|
1391
1796
|
chatId,
|
|
1392
1797
|
boundProjectKey || '',
|
|
1798
|
+
normalizeSenderId(senderId),
|
|
1393
1799
|
runtime,
|
|
1394
1800
|
onSession,
|
|
1395
1801
|
));
|
|
1802
|
+
|
|
1396
1803
|
if (sessionId) await onSession(sessionId);
|
|
1804
|
+
|
|
1805
|
+
if (runtime.name === 'codex' && requestedCodexPermissionProfile) {
|
|
1806
|
+
let observedRuntimeProfile = getActualCodexPermissionProfile(sessionId ? { id: sessionId } : session);
|
|
1807
|
+
let stabilizationRetryCount = 0;
|
|
1808
|
+
while (codexNeedsFallbackForRequestedPermissions(observedRuntimeProfile, requestedCodexPermissionProfile)
|
|
1809
|
+
&& stabilizationRetryCount < CODEX_PERMISSION_STABILIZE_MAX_RETRIES) {
|
|
1810
|
+
stabilizationRetryCount += 1;
|
|
1811
|
+
const previousSessionId = String(sessionId || session.id || '').trim();
|
|
1812
|
+
const observedSummary = observedRuntimeProfile
|
|
1813
|
+
? `${observedRuntimeProfile.sandboxMode || observedRuntimeProfile.permissionMode || 'unknown'}/${observedRuntimeProfile.approvalPolicy || 'unknown'}`
|
|
1814
|
+
: 'unknown/unknown';
|
|
1815
|
+
const requestedSummary = `${requestedCodexPermissionProfile.sandboxMode}/${requestedCodexPermissionProfile.approvalPolicy}`;
|
|
1816
|
+
log(
|
|
1817
|
+
'WARN',
|
|
1818
|
+
`Codex thread ${String(sessionId || session.id || '').slice(0, 8)} ended below requested permissions for ${sessionChatId}: ${observedSummary} vs ${requestedSummary}; retrying with a new execution thread (${stabilizationRetryCount}/${CODEX_PERMISSION_STABILIZE_MAX_RETRIES})`
|
|
1819
|
+
);
|
|
1820
|
+
session = createSession(
|
|
1821
|
+
sessionChatId,
|
|
1822
|
+
session.cwd,
|
|
1823
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1824
|
+
'codex',
|
|
1825
|
+
requestedCodexPermissionProfile
|
|
1826
|
+
);
|
|
1827
|
+
const retryRecentContext = previousSessionId && typeof getSessionRecentContext === 'function'
|
|
1828
|
+
? getSessionRecentContext(previousSessionId)
|
|
1829
|
+
: null;
|
|
1830
|
+
const freshRetryPrompt = buildCodexFallbackBridgePrompt({
|
|
1831
|
+
fullPrompt,
|
|
1832
|
+
previousSessionId,
|
|
1833
|
+
previousProfile: normalizeComparableCodexPermissionProfile(observedRuntimeProfile),
|
|
1834
|
+
requestedProfile: requestedCodexPermissionProfile,
|
|
1835
|
+
recentContext: retryRecentContext,
|
|
1836
|
+
});
|
|
1837
|
+
const freshRetryArgs = runtime.buildArgs({
|
|
1838
|
+
model,
|
|
1839
|
+
readOnly,
|
|
1840
|
+
daemonCfg,
|
|
1841
|
+
session,
|
|
1842
|
+
cwd: session.cwd,
|
|
1843
|
+
permissionProfile: requestedCodexPermissionProfile,
|
|
1844
|
+
});
|
|
1845
|
+
({
|
|
1846
|
+
output,
|
|
1847
|
+
error,
|
|
1848
|
+
errorCode,
|
|
1849
|
+
timedOut,
|
|
1850
|
+
files,
|
|
1851
|
+
toolUsageLog,
|
|
1852
|
+
sessionId,
|
|
1853
|
+
} = await spawnClaudeStreaming(
|
|
1854
|
+
freshRetryArgs,
|
|
1855
|
+
freshRetryPrompt,
|
|
1856
|
+
session.cwd,
|
|
1857
|
+
onStatus,
|
|
1858
|
+
600000,
|
|
1859
|
+
chatId,
|
|
1860
|
+
boundProjectKey || '',
|
|
1861
|
+
normalizeSenderId(senderId),
|
|
1862
|
+
runtime,
|
|
1863
|
+
onSession,
|
|
1864
|
+
));
|
|
1865
|
+
if (sessionId) await onSession(sessionId);
|
|
1866
|
+
observedRuntimeProfile = getActualCodexPermissionProfile(sessionId ? { id: sessionId } : session);
|
|
1867
|
+
}
|
|
1868
|
+
if (codexNeedsFallbackForRequestedPermissions(observedRuntimeProfile, requestedCodexPermissionProfile)) {
|
|
1869
|
+
const observedSummary = observedRuntimeProfile
|
|
1870
|
+
? `${observedRuntimeProfile.sandboxMode || observedRuntimeProfile.permissionMode || 'unknown'}/${observedRuntimeProfile.approvalPolicy || 'unknown'}`
|
|
1871
|
+
: 'unknown/unknown';
|
|
1872
|
+
const requestedSummary = `${requestedCodexPermissionProfile.sandboxMode}/${requestedCodexPermissionProfile.approvalPolicy}`;
|
|
1873
|
+
log(
|
|
1874
|
+
'WARN',
|
|
1875
|
+
`Codex thread ${String(sessionId || session.id || '').slice(0, 8)} still below requested permissions for ${sessionChatId} after ${CODEX_PERMISSION_STABILIZE_MAX_RETRIES} stabilization retries: ${observedSummary} vs ${requestedSummary}`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
const resumeFailure = classifyCodexResumeFailure(error, errorCode);
|
|
1881
|
+
if (shouldRetryCodexResumeFallback({
|
|
1882
|
+
runtimeName: runtime.name,
|
|
1883
|
+
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1884
|
+
output,
|
|
1885
|
+
error,
|
|
1886
|
+
errorCode,
|
|
1887
|
+
failureKind: resumeFailure.kind,
|
|
1888
|
+
canRetry: canRetryCodexResume(chatId, resumeFailure.kind),
|
|
1889
|
+
})) {
|
|
1890
|
+
markCodexResumeRetried(chatId, resumeFailure.kind);
|
|
1891
|
+
log(
|
|
1892
|
+
'WARN',
|
|
1893
|
+
`Codex resume failed for ${chatId}, retrying once with ${resumeFailure.kind === 'interrupted' ? 'native resume recovery' : 'fresh exec'}: ${String(error).slice(0, 120)}`
|
|
1894
|
+
);
|
|
1895
|
+
await bot.sendMessage(chatId, resumeFailure.userMessage).catch(() => { });
|
|
1896
|
+
if (resumeFailure.kind !== 'interrupted') {
|
|
1897
|
+
session = createSession(
|
|
1898
|
+
sessionChatId,
|
|
1899
|
+
session.cwd,
|
|
1900
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1901
|
+
'codex',
|
|
1902
|
+
requestedCodexPermissionProfile
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
const retryArgs = runtime.buildArgs({
|
|
1906
|
+
model,
|
|
1907
|
+
readOnly,
|
|
1908
|
+
daemonCfg,
|
|
1909
|
+
session,
|
|
1910
|
+
cwd: session.cwd,
|
|
1911
|
+
permissionProfile: requestedCodexPermissionProfile,
|
|
1912
|
+
});
|
|
1913
|
+
const retryPrompt = `${resumeFailure.retryPromptPrefix}\n\n${fullPrompt}`;
|
|
1914
|
+
({
|
|
1915
|
+
output,
|
|
1916
|
+
error,
|
|
1917
|
+
errorCode,
|
|
1918
|
+
timedOut,
|
|
1919
|
+
files,
|
|
1920
|
+
toolUsageLog,
|
|
1921
|
+
sessionId,
|
|
1922
|
+
} = await spawnClaudeStreaming(
|
|
1923
|
+
retryArgs,
|
|
1924
|
+
retryPrompt,
|
|
1925
|
+
session.cwd,
|
|
1926
|
+
onStatus,
|
|
1927
|
+
600000,
|
|
1928
|
+
chatId,
|
|
1929
|
+
boundProjectKey || '',
|
|
1930
|
+
normalizeSenderId(senderId),
|
|
1931
|
+
runtime,
|
|
1932
|
+
onSession,
|
|
1933
|
+
));
|
|
1934
|
+
if (sessionId) await onSession(sessionId);
|
|
1935
|
+
}
|
|
1936
|
+
} catch (spawnErr) {
|
|
1937
|
+
clearInterval(typingTimer);
|
|
1938
|
+
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
1939
|
+
log('ERROR', `spawnClaudeStreaming crashed for ${chatId}: ${spawnErr.message}`);
|
|
1940
|
+
await bot.sendMessage(chatId, `❌ 内部错误: ${spawnErr.message}`).catch(() => { });
|
|
1941
|
+
return { ok: false, error: spawnErr.message };
|
|
1397
1942
|
}
|
|
1398
|
-
} catch (spawnErr) {
|
|
1399
1943
|
clearInterval(typingTimer);
|
|
1400
|
-
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
1401
|
-
log('ERROR', `spawnClaudeStreaming crashed for ${chatId}: ${spawnErr.message}`);
|
|
1402
|
-
await bot.sendMessage(chatId, `❌ 内部错误: ${spawnErr.message}`).catch(() => { });
|
|
1403
|
-
return { ok: false, error: spawnErr.message };
|
|
1404
|
-
}
|
|
1405
|
-
clearInterval(typingTimer);
|
|
1406
1944
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
skillEvolution.
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1945
|
+
// [PROTECTED] L0 lossless diary — see logRawSessionDiary() at file top
|
|
1946
|
+
logRawSessionDiary(fs, path, HOME, { chatId, prompt, output, error, projectKey: boundProjectKey });
|
|
1947
|
+
|
|
1948
|
+
// Skill evolution: capture signal + hot path heuristic check
|
|
1949
|
+
if (skillEvolution) {
|
|
1950
|
+
try {
|
|
1951
|
+
const signal = skillEvolution.extractSkillSignal(fullPrompt, output, error, files, session.cwd, toolUsageLog);
|
|
1952
|
+
if (signal) {
|
|
1953
|
+
skillEvolution.appendSkillSignal(signal);
|
|
1954
|
+
skillEvolution.checkHotEvolution(signal);
|
|
1955
|
+
}
|
|
1956
|
+
} catch (e) { log('WARN', `Skill evolution signal capture failed: ${e.message}`); }
|
|
1957
|
+
}
|
|
1417
1958
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1959
|
+
// statusMsgId is always available for final reply handling (edit or delete).
|
|
1960
|
+
const _statusMsgIdForReply = statusMsgId || null;
|
|
1420
1961
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1962
|
+
// Mentor post-flight debt registration (intense mode only).
|
|
1963
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed && mentorEngine && typeof mentorEngine.registerDebt === 'function' && output) {
|
|
1964
|
+
try {
|
|
1965
|
+
const mode = resolveMentorMode(mentorCfg);
|
|
1966
|
+
if (mode === 'intense') {
|
|
1967
|
+
const codeLines = countCodeLines(output);
|
|
1968
|
+
if (codeLines > 30) {
|
|
1969
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1970
|
+
const projectId = info && info.project_id ? info.project_id : 'proj_default';
|
|
1971
|
+
mentorEngine.registerDebt(projectId, String(prompt || '').slice(0, 120), codeLines);
|
|
1972
|
+
log('INFO', `[MENTOR] Registered reflection debt (${projectId}, lines=${codeLines})`);
|
|
1973
|
+
}
|
|
1432
1974
|
}
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
log('WARN', `Mentor post-flight failed: ${e.message}`);
|
|
1433
1977
|
}
|
|
1434
|
-
} catch (e) {
|
|
1435
|
-
log('WARN', `Mentor post-flight failed: ${e.message}`);
|
|
1436
1978
|
}
|
|
1437
|
-
}
|
|
1438
1979
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1980
|
+
// When Claude completes with no text output (pure tool work), send a done notice
|
|
1981
|
+
if (output === '' && !error) {
|
|
1982
|
+
// Special case: if dispatch_to was called, send a "forwarded" confirmation
|
|
1983
|
+
const dispatchedTargets = (toolUsageLog || [])
|
|
1984
|
+
.filter(t => t.tool === 'Bash' && typeof t.context === 'string' && t.context.includes('dispatch_to'))
|
|
1985
|
+
.map(t => { const m = t.context.match(/dispatch_to\s+(\S+)/); return m ? m[1] : null; })
|
|
1986
|
+
.filter(Boolean);
|
|
1987
|
+
if (dispatchedTargets.length > 0) {
|
|
1988
|
+
const allProjects = (config && config.projects) || {};
|
|
1989
|
+
const names = dispatchedTargets.map(k => (allProjects[k] && allProjects[k].name) || k).join('、');
|
|
1990
|
+
const doneMsg = await bot.sendMessage(chatId, `✉️ 已转达给 ${names},处理中…`);
|
|
1991
|
+
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1992
|
+
const wasNew = !session.started;
|
|
1993
|
+
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1994
|
+
return { ok: true };
|
|
1995
|
+
}
|
|
1996
|
+
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
1997
|
+
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
1450
1998
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1451
1999
|
const wasNew = !session.started;
|
|
1452
2000
|
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1453
2001
|
return { ok: true };
|
|
1454
2002
|
}
|
|
1455
|
-
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
1456
|
-
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
1457
|
-
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1458
|
-
const wasNew = !session.started;
|
|
1459
|
-
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1460
|
-
return { ok: true };
|
|
1461
|
-
}
|
|
1462
2003
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
2004
|
+
if (output) {
|
|
2005
|
+
if (runtime.name === 'codex') {
|
|
2006
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'interrupted'));
|
|
2007
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'expired'));
|
|
2008
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'default'));
|
|
2009
|
+
}
|
|
2010
|
+
// Detect provider/model errors disguised as output (e.g., "model not found", API errors)
|
|
2011
|
+
if (runtime.name === 'claude') {
|
|
2012
|
+
const activeProvCheck = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
2013
|
+
const builtinModelsCheck = ['sonnet', 'opus', 'haiku'];
|
|
2014
|
+
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
2015
|
+
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
2016
|
+
try {
|
|
2017
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
2018
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
2019
|
+
} catch (fbErr) {
|
|
2020
|
+
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
2021
|
+
await bot.sendMarkdown(chatId, output);
|
|
2022
|
+
}
|
|
2023
|
+
return { ok: false, error: output };
|
|
1477
2024
|
}
|
|
1478
|
-
return { ok: false, error: output };
|
|
1479
2025
|
}
|
|
1480
|
-
}
|
|
1481
2026
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
2027
|
+
// Mark session as started after first successful call
|
|
2028
|
+
const wasNew = !session.started;
|
|
2029
|
+
if (wasNew) {
|
|
2030
|
+
markSessionStarted(sessionChatId, engineName);
|
|
2031
|
+
if (runtime.name === 'codex' && session.runtimeSessionObserved === false) {
|
|
2032
|
+
log('WARN', `Codex completed without emitting thread id for ${chatId}; keeping session non-resumable until a real thread id is observed`);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1485
2035
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2036
|
+
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
2037
|
+
const chatCategory = classifyChatUsage(chatId, {
|
|
2038
|
+
projectKey: boundProjectKey || '',
|
|
2039
|
+
cwd: session && session.cwd,
|
|
2040
|
+
homeDir: HOME,
|
|
2041
|
+
});
|
|
2042
|
+
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
1493
2043
|
|
|
1494
|
-
|
|
1495
|
-
|
|
2044
|
+
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
2045
|
+
let { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
1496
2046
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2047
|
+
// Timeout with partial results: prepend warning
|
|
2048
|
+
if (timedOut) {
|
|
2049
|
+
cleanOutput = `⚠️ **任务超时,以下是已完成的部分结果:**\n\n${cleanOutput}`;
|
|
2050
|
+
}
|
|
1501
2051
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2052
|
+
// Match current session to a project for colored card display.
|
|
2053
|
+
// Prefer the bound project (known by virtual chatId or chat_agent_map) — avoids ambiguity
|
|
2054
|
+
// when multiple projects share the same cwd (e.g. team members with parent project cwd).
|
|
2055
|
+
let activeProject = boundProject || null;
|
|
2056
|
+
if (!activeProject && session && session.cwd && config && config.projects) {
|
|
2057
|
+
const sessionCwd = path.resolve(normalizeCwd(session.cwd));
|
|
2058
|
+
for (const [, proj] of Object.entries(config.projects)) {
|
|
2059
|
+
if (!proj.cwd) continue;
|
|
2060
|
+
const projCwd = path.resolve(normalizeCwd(proj.cwd));
|
|
2061
|
+
if (sessionCwd === projCwd) { activeProject = proj; break; }
|
|
2062
|
+
}
|
|
1512
2063
|
}
|
|
1513
|
-
}
|
|
1514
2064
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2065
|
+
let replyMsg;
|
|
2066
|
+
try {
|
|
2067
|
+
log('DEBUG', `[REPLY:${chatId}] statusMsgId=${statusMsgId} editFailed=${editFailed} activeProject=${activeProject && activeProject.name} lastCard=${_lastStatusCardContent ? _lastStatusCardContent.slice(0, 40) : 'null'}`);
|
|
2068
|
+
|
|
2069
|
+
// siri_ask: write full response to temp file for any dispatch-triggered reply
|
|
2070
|
+
if (chatId && chatId.startsWith('_agent_') && cleanOutput) {
|
|
2071
|
+
try { require('fs').writeFileSync('/tmp/siri_response.txt', cleanOutput); } catch {}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Strategy: always try to update the status card first (avoids sending a new card
|
|
2075
|
+
// while the old 🤔 card lingers, which would produce two messages).
|
|
2076
|
+
// If edit fails: try to delete the status card (awaited, not fire-and-forget).
|
|
2077
|
+
// If delete also fails: fall through to sending a new card.
|
|
2078
|
+
if (_statusMsgIdForReply && bot.editMessage) {
|
|
2079
|
+
// Skip redundant edit: streaming already wrote the final content to the card.
|
|
2080
|
+
// _lastStatusCardContent tracks the last __STREAM_TEXT__ write, so if it matches
|
|
2081
|
+
// cleanOutput the card is already showing the right content — no update needed.
|
|
2082
|
+
if (_lastStatusCardContent !== null && _lastStatusCardContent === cleanOutput) {
|
|
2083
|
+
log('DEBUG', `[REPLY:${chatId}] skipping editMessage — card already shows final content`);
|
|
1534
2084
|
replyMsg = { message_id: _statusMsgIdForReply };
|
|
1535
|
-
} else
|
|
1536
|
-
const
|
|
1537
|
-
log('DEBUG', `[REPLY:${chatId}]
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
2085
|
+
} else {
|
|
2086
|
+
const editOk = await bot.editMessage(chatId, _statusMsgIdForReply, cleanOutput, _ackCardHeader);
|
|
2087
|
+
log('DEBUG', `[REPLY:${chatId}] editMessage result=${editOk}`);
|
|
2088
|
+
if (editOk !== false) {
|
|
2089
|
+
replyMsg = { message_id: _statusMsgIdForReply };
|
|
2090
|
+
} else if (bot.deleteMessage) {
|
|
2091
|
+
const deleted = await bot.deleteMessage(chatId, _statusMsgIdForReply).then(() => true).catch(() => false);
|
|
2092
|
+
log('DEBUG', `[REPLY:${chatId}] deleteMessage result=${deleted}`);
|
|
2093
|
+
if (!deleted) {
|
|
2094
|
+
// Both edit and delete failed — try one more edit attempt to avoid leaving 🤔
|
|
2095
|
+
log('WARN', `[REPLY:${chatId}] deleteMessage failed — status card may linger alongside new reply`);
|
|
2096
|
+
}
|
|
1541
2097
|
}
|
|
1542
2098
|
}
|
|
2099
|
+
} else if (_statusMsgIdForReply && bot.deleteMessage) {
|
|
2100
|
+
// No editMessage — delete the status card
|
|
2101
|
+
await bot.deleteMessage(chatId, _statusMsgIdForReply).catch(() => { });
|
|
1543
2102
|
}
|
|
1544
|
-
} else if (_statusMsgIdForReply && bot.deleteMessage) {
|
|
1545
|
-
// No editMessage — delete the status card
|
|
1546
|
-
await bot.deleteMessage(chatId, _statusMsgIdForReply).catch(() => { });
|
|
1547
|
-
}
|
|
1548
2103
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2104
|
+
if (!replyMsg) {
|
|
2105
|
+
if (activeProject && bot.sendCard) {
|
|
2106
|
+
log('DEBUG', `[REPLY:${chatId}] sending sendCard`);
|
|
2107
|
+
replyMsg = await bot.sendCard(chatId, {
|
|
2108
|
+
title: `${activeProject.icon || '🤖'} ${activeProject.name || ''}`,
|
|
2109
|
+
body: cleanOutput,
|
|
2110
|
+
color: activeProject.color || 'blue',
|
|
2111
|
+
});
|
|
2112
|
+
log('DEBUG', `[REPLY:${chatId}] sendCard done msgId=${replyMsg && replyMsg.message_id}`);
|
|
2113
|
+
} else {
|
|
2114
|
+
log('DEBUG', `[REPLY:${chatId}] sending sendMarkdown`);
|
|
2115
|
+
replyMsg = await bot.sendMarkdown(chatId, cleanOutput);
|
|
2116
|
+
log('DEBUG', `[REPLY:${chatId}] sendMarkdown done msgId=${replyMsg && replyMsg.message_id}`);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
} catch (sendErr) {
|
|
2120
|
+
log('WARN', `sendCard/sendMarkdown failed (${sendErr.message}), falling back to sendMessage`);
|
|
2121
|
+
try { replyMsg = await bot.sendMessage(chatId, cleanOutput); } catch (e2) {
|
|
2122
|
+
log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
|
|
1562
2123
|
}
|
|
1563
2124
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2125
|
+
const trackedAgentKey = String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null;
|
|
2126
|
+
if (replyMsg && replyMsg.message_id && session) {
|
|
2127
|
+
if (runtime.name === 'codex' && session.runtimeSessionObserved === false) {
|
|
2128
|
+
trackMsgSession(replyMsg.message_id, session, trackedAgentKey, { routeOnly: true });
|
|
2129
|
+
} else {
|
|
2130
|
+
trackMsgSession(replyMsg.message_id, session, trackedAgentKey);
|
|
2131
|
+
}
|
|
1568
2132
|
}
|
|
1569
|
-
}
|
|
1570
|
-
if (replyMsg && replyMsg.message_id && session) trackMsgSession(replyMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1571
|
-
|
|
1572
|
-
await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
|
|
1573
2133
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
2134
|
+
const fileMsgs = await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
|
|
2135
|
+
if (session && Array.isArray(fileMsgs)) {
|
|
2136
|
+
for (const msg of fileMsgs) {
|
|
2137
|
+
if (!msg || !msg.message_id) continue;
|
|
2138
|
+
if (runtime.name === 'codex' && session.runtimeSessionObserved === false) {
|
|
2139
|
+
trackMsgSession(msg.message_id, session, trackedAgentKey, { routeOnly: true });
|
|
2140
|
+
} else {
|
|
2141
|
+
trackMsgSession(msg.message_id, session, trackedAgentKey);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1578
2145
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2146
|
+
// Timeout: also send the reason after the partial result
|
|
2147
|
+
if (timedOut && error) {
|
|
2148
|
+
try { await bot.sendMessage(chatId, error); } catch { /* */ }
|
|
2149
|
+
}
|
|
1583
2150
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const memory = require('./memory');
|
|
1589
|
-
const pKey = boundProjectKey || '';
|
|
1590
|
-
const sessions = memory.recentSessions({ limit: 5, project: pKey });
|
|
1591
|
-
const factsRaw = memory.searchFacts('', { limit: 10, project: pKey });
|
|
1592
|
-
const facts = Array.isArray(factsRaw) ? factsRaw : [];
|
|
1593
|
-
memory.close();
|
|
1594
|
-
const snapshotContent = buildMemorySnapshotContent(sessions, facts);
|
|
1595
|
-
const agentId = boundProject.agent_id;
|
|
1596
|
-
if (refreshMemorySnapshot(agentId, snapshotContent, HOME)) {
|
|
1597
|
-
log('DEBUG', `[AGENT] Memory snapshot refreshed for ${agentId}`);
|
|
1598
|
-
}
|
|
1599
|
-
} catch { /* non-critical — memory module may not be available */ }
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
return { ok: !timedOut };
|
|
1603
|
-
} else {
|
|
1604
|
-
const errMsg = error || 'Unknown error';
|
|
1605
|
-
const userErrMsg = (errorCode === 'AUTH_REQUIRED' || errorCode === 'RATE_LIMIT')
|
|
1606
|
-
? errMsg
|
|
1607
|
-
: `Error: ${errMsg.slice(0, 200)}`;
|
|
1608
|
-
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
1609
|
-
|
|
1610
|
-
// If session not found (expired/deleted), create new and retry once (Claude path)
|
|
1611
|
-
if (runtime.name === 'claude' && (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use'))) {
|
|
1612
|
-
log('WARN', `Session ${session.id} unusable (${errMsg.includes('already in use') ? 'locked' : 'not found'}), creating new`);
|
|
1613
|
-
session = createSession(sessionChatId, session.cwd, '', runtime.name);
|
|
1614
|
-
|
|
1615
|
-
const retryArgs = runtime.buildArgs({
|
|
1616
|
-
model,
|
|
1617
|
-
readOnly,
|
|
1618
|
-
daemonCfg,
|
|
1619
|
-
session,
|
|
1620
|
-
cwd: session.cwd,
|
|
1621
|
-
});
|
|
2151
|
+
// Auto-name: if this was the first message and session has no name, generate one
|
|
2152
|
+
if (runtime.name === 'claude' && wasNew && !getSessionName(session.id)) {
|
|
2153
|
+
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
2154
|
+
}
|
|
1622
2155
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
return { ok: true };
|
|
1641
|
-
} else {
|
|
1642
|
-
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
1643
|
-
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1644
|
-
return { ok: false, error: retry.error || errMsg };
|
|
2156
|
+
// Auto-refresh memory-snapshot.md for this agent on first session message (fire-and-forget)
|
|
2157
|
+
if (wasNew && boundProject && boundProject.agent_id) {
|
|
2158
|
+
setImmediate(async () => {
|
|
2159
|
+
try {
|
|
2160
|
+
const memory = require('./memory');
|
|
2161
|
+
const pKey = boundProjectKey || '';
|
|
2162
|
+
const sessions = memory.recentSessions({ limit: 5, project: pKey });
|
|
2163
|
+
const factsRaw = memory.searchFacts('', { limit: 10, project: pKey });
|
|
2164
|
+
const facts = Array.isArray(factsRaw) ? factsRaw : [];
|
|
2165
|
+
memory.close();
|
|
2166
|
+
const snapshotContent = buildMemorySnapshotContent(sessions, facts);
|
|
2167
|
+
const agentId = boundProject.agent_id;
|
|
2168
|
+
if (refreshMemorySnapshot(agentId, snapshotContent, HOME)) {
|
|
2169
|
+
log('DEBUG', `[AGENT] Memory snapshot refreshed for ${agentId}`);
|
|
2170
|
+
}
|
|
2171
|
+
} catch { /* non-critical — memory module may not be available */ }
|
|
2172
|
+
});
|
|
1645
2173
|
}
|
|
2174
|
+
return { ok: !timedOut };
|
|
1646
2175
|
} else {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
2176
|
+
const errMsg = error || 'Unknown error';
|
|
2177
|
+
const userErrMsg = (errorCode === 'AUTH_REQUIRED' || errorCode === 'RATE_LIMIT')
|
|
2178
|
+
? errMsg
|
|
2179
|
+
: `Error: ${errMsg.slice(0, 200)}`;
|
|
2180
|
+
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
2181
|
+
|
|
2182
|
+
// If session not found / locked / thinking signature invalid — create new and retry once (Claude path)
|
|
2183
|
+
const _isThinkingSignatureError = isClaudeThinkingSignatureError(errMsg);
|
|
2184
|
+
const _isSessionResumeFail = errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use') || _isThinkingSignatureError;
|
|
2185
|
+
if (runtime.name === 'claude' && _isSessionResumeFail) {
|
|
2186
|
+
const _reason = errMsg.includes('already in use') ? 'locked' : _isThinkingSignatureError ? 'thinking-signature-invalid' : 'not found';
|
|
2187
|
+
log('WARN', `Session ${session.id} unusable (${_reason}), creating new`);
|
|
2188
|
+
session = createSession(sessionChatId, session.cwd, '', runtime.name);
|
|
2189
|
+
|
|
2190
|
+
const retryArgs = runtime.buildArgs({
|
|
2191
|
+
model,
|
|
2192
|
+
readOnly,
|
|
2193
|
+
daemonCfg,
|
|
2194
|
+
session,
|
|
2195
|
+
cwd: session.cwd,
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
const retry = await spawnClaudeStreaming(
|
|
2199
|
+
retryArgs,
|
|
2200
|
+
fullPrompt,
|
|
2201
|
+
session.cwd,
|
|
2202
|
+
onStatus,
|
|
2203
|
+
600000,
|
|
2204
|
+
chatId,
|
|
2205
|
+
boundProjectKey || '',
|
|
2206
|
+
normalizeSenderId(senderId),
|
|
2207
|
+
runtime,
|
|
2208
|
+
onSession,
|
|
2209
|
+
);
|
|
2210
|
+
if (retry.sessionId) await onSession(retry.sessionId);
|
|
2211
|
+
if (retry.output) {
|
|
2212
|
+
markSessionStarted(sessionChatId, runtime.name);
|
|
2213
|
+
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
2214
|
+
await bot.sendMarkdown(chatId, retryClean);
|
|
2215
|
+
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
2216
|
+
return { ok: true };
|
|
2217
|
+
} else {
|
|
2218
|
+
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
2219
|
+
const retryUserMsg = _isThinkingSignatureError
|
|
2220
|
+
? formatClaudeResumeFallbackUserMessage(retry.error || errMsg)
|
|
2221
|
+
: userErrMsg;
|
|
2222
|
+
try { await bot.sendMessage(chatId, retryUserMsg); } catch { /* */ }
|
|
2223
|
+
return { ok: false, error: retry.error || errMsg };
|
|
2224
|
+
}
|
|
2225
|
+
} else {
|
|
2226
|
+
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus (Claude path only)
|
|
2227
|
+
if (runtime.name === 'claude') {
|
|
2228
|
+
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
2229
|
+
const builtinModels = ENGINE_MODEL_CONFIG.claude.options;
|
|
2230
|
+
if ((activeProv !== 'anthropic' || !builtinModels.includes(model)) && !errMsg.includes('Stopped by user')) {
|
|
2231
|
+
try {
|
|
2232
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
2233
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
2234
|
+
} catch (fallbackErr) {
|
|
2235
|
+
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
2236
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
2237
|
+
}
|
|
2238
|
+
} else {
|
|
1657
2239
|
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1658
2240
|
}
|
|
1659
2241
|
} else {
|
|
1660
2242
|
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1661
2243
|
}
|
|
1662
|
-
|
|
1663
|
-
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
2244
|
+
return { ok: false, error: errMsg, errorCode };
|
|
1664
2245
|
}
|
|
1665
|
-
return { ok: false, error: errMsg, errorCode };
|
|
1666
2246
|
}
|
|
1667
|
-
}
|
|
1668
2247
|
|
|
1669
2248
|
} catch (fatalErr) { // ── safety-net-catch ──
|
|
1670
2249
|
clearInterval(typingTimer);
|
|
@@ -1687,9 +2266,23 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1687
2266
|
shouldRetryCodexResumeFallback,
|
|
1688
2267
|
formatEngineSpawnError,
|
|
1689
2268
|
adaptDaemonHintForEngine,
|
|
2269
|
+
getSessionChatId,
|
|
2270
|
+
getCodexPermissionProfile,
|
|
2271
|
+
getActualCodexPermissionProfile,
|
|
2272
|
+
sameCodexPermissionProfile,
|
|
2273
|
+
inspectClaudeResumeSession,
|
|
2274
|
+
isClaudeThinkingSignatureError,
|
|
2275
|
+
formatClaudeResumeFallbackUserMessage,
|
|
2276
|
+
classifyCodexResumeFailure,
|
|
1690
2277
|
canRetryCodexResume,
|
|
1691
2278
|
markCodexResumeRetried,
|
|
2279
|
+
getCodexResumeRetryKey,
|
|
1692
2280
|
CODEX_RESUME_RETRY_WINDOW_MS,
|
|
2281
|
+
shouldAutoRouteSkill,
|
|
2282
|
+
codexSandboxPrivilegeRank,
|
|
2283
|
+
codexApprovalPrivilegeRank,
|
|
2284
|
+
codexNeedsFallbackForRequestedPermissions,
|
|
2285
|
+
buildCodexFallbackBridgePrompt,
|
|
1693
2286
|
},
|
|
1694
2287
|
};
|
|
1695
2288
|
}
|