metame-cli 1.5.4 → 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 +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- 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 +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- 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 +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
|
@@ -2,7 +2,14 @@
|
|
|
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
|
|
|
8
15
|
/**
|
|
@@ -37,7 +44,6 @@ function createClaudeEngine(deps) {
|
|
|
37
44
|
getActiveProviderEnv,
|
|
38
45
|
activeProcesses,
|
|
39
46
|
saveActivePids,
|
|
40
|
-
messageQueue,
|
|
41
47
|
log,
|
|
42
48
|
yaml,
|
|
43
49
|
providerMod,
|
|
@@ -52,7 +58,6 @@ function createClaudeEngine(deps) {
|
|
|
52
58
|
isContentFile,
|
|
53
59
|
sendFileButtons,
|
|
54
60
|
findSessionFile,
|
|
55
|
-
listRecentSessions,
|
|
56
61
|
getSession,
|
|
57
62
|
getSessionForEngine,
|
|
58
63
|
createSession,
|
|
@@ -60,6 +65,9 @@ function createClaudeEngine(deps) {
|
|
|
60
65
|
writeSessionName,
|
|
61
66
|
markSessionStarted,
|
|
62
67
|
isEngineSessionValid,
|
|
68
|
+
getCodexSessionSandboxProfile,
|
|
69
|
+
getCodexSessionPermissionMode,
|
|
70
|
+
getSessionRecentContext,
|
|
63
71
|
gitCheckpoint,
|
|
64
72
|
gitCheckpointAsync,
|
|
65
73
|
recordTokens,
|
|
@@ -73,11 +81,42 @@ function createClaudeEngine(deps) {
|
|
|
73
81
|
function getDefaultEngine() {
|
|
74
82
|
return (typeof _getDefaultEngine === 'function') ? _getDefaultEngine() : 'claude';
|
|
75
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
|
+
}
|
|
76
106
|
let mentorEngine = null;
|
|
77
107
|
try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
|
|
78
108
|
let sessionAnalytics = null;
|
|
79
109
|
try { sessionAnalytics = require('./session-analytics'); } catch { /* optional */ }
|
|
80
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
|
+
|
|
81
120
|
const getEngineRuntime = typeof injectedGetEngineRuntime === 'function'
|
|
82
121
|
? injectedGetEngineRuntime
|
|
83
122
|
: createEngineRuntimeFactory({ fs, path, HOME, CLAUDE_BIN, getActiveProviderEnv });
|
|
@@ -147,7 +186,11 @@ function createClaudeEngine(deps) {
|
|
|
147
186
|
if (!state.sessions) state.sessions = {};
|
|
148
187
|
const cur = state.sessions[chatId] || {};
|
|
149
188
|
const patched = typeof patchFn === 'function' ? patchFn(cur) : cur;
|
|
150
|
-
|
|
189
|
+
if (patched && typeof patched === 'object') {
|
|
190
|
+
state.sessions[chatId] = { ...patched, last_active: Date.now() };
|
|
191
|
+
} else {
|
|
192
|
+
state.sessions[chatId] = cur;
|
|
193
|
+
}
|
|
151
194
|
saveState(state);
|
|
152
195
|
}).catch((e) => {
|
|
153
196
|
log('WARN', `patchSessionSerialized failed for ${chatId}: ${e.message}`);
|
|
@@ -159,27 +202,35 @@ function createClaudeEngine(deps) {
|
|
|
159
202
|
}
|
|
160
203
|
|
|
161
204
|
const CODEX_RESUME_RETRY_WINDOW_MS = 10 * 60 * 1000;
|
|
162
|
-
const
|
|
205
|
+
const CODEX_PERMISSION_STABILIZE_MAX_RETRIES = 2;
|
|
206
|
+
const _codexResumeRetryTs = new Map(); // `${chatId}:${kind}` -> last retry ts
|
|
207
|
+
|
|
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
|
+
}
|
|
163
213
|
|
|
164
|
-
function canRetryCodexResume(chatId) {
|
|
165
|
-
const key =
|
|
214
|
+
function canRetryCodexResume(chatId, kind = 'default') {
|
|
215
|
+
const key = getCodexResumeRetryKey(chatId, kind);
|
|
166
216
|
if (!key) return false;
|
|
167
217
|
const last = Number(_codexResumeRetryTs.get(key) || 0);
|
|
168
218
|
if (!last) return true;
|
|
169
219
|
return (Date.now() - last) > CODEX_RESUME_RETRY_WINDOW_MS;
|
|
170
220
|
}
|
|
171
221
|
|
|
172
|
-
function markCodexResumeRetried(chatId) {
|
|
173
|
-
const key =
|
|
222
|
+
function markCodexResumeRetried(chatId, kind = 'default') {
|
|
223
|
+
const key = getCodexResumeRetryKey(chatId, kind);
|
|
174
224
|
if (!key) return;
|
|
175
225
|
_codexResumeRetryTs.set(key, Date.now());
|
|
176
226
|
}
|
|
177
227
|
|
|
178
|
-
function shouldRetryCodexResumeFallback({ runtimeName, wasResumeAttempt, output, error, errorCode, canRetry }) {
|
|
228
|
+
function shouldRetryCodexResumeFallback({ runtimeName, wasResumeAttempt, output, error, errorCode, canRetry, failureKind = '' }) {
|
|
179
229
|
return runtimeName === 'codex'
|
|
180
230
|
&& !!wasResumeAttempt
|
|
181
231
|
&& !!error
|
|
182
232
|
&& (!output || !!errorCode)
|
|
233
|
+
&& failureKind !== 'user-stop'
|
|
183
234
|
&& !!canRetry;
|
|
184
235
|
}
|
|
185
236
|
|
|
@@ -205,6 +256,218 @@ function createClaudeEngine(deps) {
|
|
|
205
256
|
return out;
|
|
206
257
|
}
|
|
207
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
|
+
|
|
208
471
|
|
|
209
472
|
/**
|
|
210
473
|
* Parse [[FILE:...]] markers from Claude output.
|
|
@@ -497,6 +760,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
497
760
|
timeoutMs = 600000,
|
|
498
761
|
chatId = null,
|
|
499
762
|
metameProject = '',
|
|
763
|
+
metameSenderId = '',
|
|
500
764
|
runtime = null,
|
|
501
765
|
onSession = null,
|
|
502
766
|
) {
|
|
@@ -516,7 +780,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
516
780
|
cwd,
|
|
517
781
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
518
782
|
detached: process.platform !== 'win32',
|
|
519
|
-
env: rt.buildEnv({ metameProject }),
|
|
783
|
+
env: rt.buildEnv({ metameProject, metameSenderId }),
|
|
520
784
|
});
|
|
521
785
|
log('INFO', `[TIMING:${chatId}] spawned ${rt.name} pid=${child.pid}`);
|
|
522
786
|
|
|
@@ -524,6 +788,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
524
788
|
activeProcesses.set(chatId, {
|
|
525
789
|
child,
|
|
526
790
|
aborted: false,
|
|
791
|
+
abortReason: null,
|
|
527
792
|
startedAt: _spawnAt,
|
|
528
793
|
engine: rt.name,
|
|
529
794
|
killSignal: rt.killSignal || 'SIGTERM',
|
|
@@ -556,6 +821,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
556
821
|
const writtenFiles = [];
|
|
557
822
|
const toolUsageLog = [];
|
|
558
823
|
|
|
824
|
+
void timeoutMs;
|
|
559
825
|
const engineTimeouts = rt.timeouts || {};
|
|
560
826
|
const IDLE_TIMEOUT_MS = engineTimeouts.idleMs || (5 * 60 * 1000);
|
|
561
827
|
const TOOL_EXEC_TIMEOUT_MS = engineTimeouts.toolMs || (25 * 60 * 1000);
|
|
@@ -776,10 +1042,21 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
776
1042
|
|
|
777
1043
|
const proc = chatId ? activeProcesses.get(chatId) : null;
|
|
778
1044
|
const wasAborted = proc && proc.aborted;
|
|
1045
|
+
const abortReason = proc && proc.abortReason ? String(proc.abortReason) : '';
|
|
779
1046
|
if (chatId) { activeProcesses.delete(chatId); saveActivePids(); }
|
|
780
1047
|
|
|
781
1048
|
if (wasAborted) {
|
|
782
|
-
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
|
+
});
|
|
783
1060
|
return;
|
|
784
1061
|
}
|
|
785
1062
|
if (killed) {
|
|
@@ -833,11 +1110,22 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
833
1110
|
|
|
834
1111
|
// Track outbound message_id → session for reply-based session restoration.
|
|
835
1112
|
// Keeps last 200 entries to avoid unbounded growth.
|
|
836
|
-
function trackMsgSession(messageId, session, agentKey) {
|
|
837
|
-
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;
|
|
838
1117
|
const st = loadState();
|
|
839
1118
|
if (!st.msg_sessions) st.msg_sessions = {};
|
|
840
|
-
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
|
+
};
|
|
841
1129
|
const keys = Object.keys(st.msg_sessions);
|
|
842
1130
|
if (keys.length > 200) {
|
|
843
1131
|
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
@@ -866,7 +1154,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
866
1154
|
return loadConfig();
|
|
867
1155
|
}
|
|
868
1156
|
|
|
869
|
-
async function askClaude(bot, chatId, prompt, config, readOnly = false) {
|
|
1157
|
+
async function askClaude(bot, chatId, prompt, config, readOnly = false, senderId = null) {
|
|
870
1158
|
const _t0 = Date.now();
|
|
871
1159
|
log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
|
|
872
1160
|
// Track interaction time for idle/sleep detection
|
|
@@ -885,7 +1173,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
885
1173
|
let _lastStatusCardContent = null; // tracks last clean text written to card (for final-reply dedup)
|
|
886
1174
|
// Early detect bound project for branded ack card (team members / dispatch agents)
|
|
887
1175
|
const _ackChatIdStr = String(chatId);
|
|
888
|
-
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
|
+
};
|
|
889
1181
|
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
890
1182
|
const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
|
|
891
1183
|
// _ackCardHeader: non-null for agents with icon/name (team members, dispatch); passed to editMessage to preserve header on streaming edits
|
|
@@ -895,12 +1187,14 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
895
1187
|
// Fire-and-forget: don't await Telegram RTT before spawning the engine process.
|
|
896
1188
|
// statusMsgId will be populated well before the first model output (~5s for codex).
|
|
897
1189
|
// For branded agents: send a card with header so streaming edits preserve the agent identity.
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
}
|
|
904
1198
|
bot.sendTyping(chatId).catch(() => { });
|
|
905
1199
|
const typingTimer = setInterval(() => {
|
|
906
1200
|
bot.sendTyping(chatId).catch(() => { });
|
|
@@ -913,7 +1207,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
913
1207
|
|
|
914
1208
|
// Agent nickname routing: "贾维斯" / "小美,帮我..." → switch project session
|
|
915
1209
|
// Strict chats (chat_agent_map bound groups) must NOT switch agents via nickname
|
|
916
|
-
const _strictAgentMap = {
|
|
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
|
+
};
|
|
917
1215
|
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
918
1216
|
const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
|
|
919
1217
|
if (agentMatch) {
|
|
@@ -935,12 +1233,17 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
935
1233
|
// BUT: skip skill routing if agent addressed by nickname OR chat already has an active session
|
|
936
1234
|
// (active conversation should never be hijacked by keyword-based skill matching)
|
|
937
1235
|
const chatIdStr = String(chatId);
|
|
938
|
-
const chatAgentMap = {
|
|
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
|
+
};
|
|
939
1241
|
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
940
1242
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
941
|
-
|
|
942
|
-
//
|
|
943
|
-
|
|
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);
|
|
944
1247
|
const sessionRaw = getSession(sessionChatId);
|
|
945
1248
|
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
946
1249
|
const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
|
|
@@ -950,40 +1253,85 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
950
1253
|
(boundProject && boundProject.engine) || getDefaultEngine()
|
|
951
1254
|
);
|
|
952
1255
|
const runtime = getEngineRuntime(engineName);
|
|
1256
|
+
const requestedCodexPermissionProfile = engineName === 'codex'
|
|
1257
|
+
? getCodexPermissionProfile(readOnly, daemonCfg)
|
|
1258
|
+
: null;
|
|
953
1259
|
|
|
954
1260
|
// hasActiveSession: does the current engine have an ongoing conversation?
|
|
955
1261
|
const hasActiveSession = sessionRaw && (
|
|
956
1262
|
sessionRaw.engines ? !!(sessionRaw.engines[engineName]?.started) : !!sessionRaw.started
|
|
957
1263
|
);
|
|
958
|
-
const
|
|
1264
|
+
const detectedSkill = routeSkill(prompt);
|
|
1265
|
+
const skill = shouldAutoRouteSkill({
|
|
1266
|
+
agentMatch,
|
|
1267
|
+
hasActiveSession,
|
|
1268
|
+
boundProjectKey,
|
|
1269
|
+
skillName: detectedSkill,
|
|
1270
|
+
})
|
|
1271
|
+
? detectedSkill
|
|
1272
|
+
: null;
|
|
959
1273
|
|
|
960
1274
|
if (!sessionRaw) {
|
|
961
1275
|
// No saved state for this chatId: start a fresh session.
|
|
962
1276
|
// Note: daemon_state.json persists across restarts, so this only happens on truly first use
|
|
963
1277
|
// or after an explicit /new command.
|
|
964
|
-
createSession(
|
|
1278
|
+
createSession(
|
|
1279
|
+
sessionChatId,
|
|
1280
|
+
boundCwd || undefined,
|
|
1281
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1282
|
+
boundEngineName,
|
|
1283
|
+
boundEngineName === 'codex' ? requestedCodexPermissionProfile : undefined
|
|
1284
|
+
);
|
|
965
1285
|
}
|
|
966
1286
|
|
|
967
1287
|
// Resolve flat view for current engine (id + started are engine-specific; cwd is shared)
|
|
968
|
-
let session =
|
|
1288
|
+
let session = resolveSessionForEngine(sessionChatId, engineName) || { cwd: boundCwd || HOME, engine: engineName, id: null, started: false };
|
|
969
1289
|
session.engine = engineName; // keep local copy for Codex resume detection below
|
|
1290
|
+
session.logicalChatId = sessionChatId;
|
|
970
1291
|
|
|
971
1292
|
// Pre-spawn session validation: unified for all engines.
|
|
972
1293
|
// Claude checks JSONL file existence; Codex checks SQLite. Same interface, different backend.
|
|
973
1294
|
// Skip warning for virtual agents (team members) - they may use worktrees with fresh sessions
|
|
974
1295
|
const isVirtualAgent = String(sessionChatId).startsWith('_agent_');
|
|
975
1296
|
if (session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
976
|
-
const valid =
|
|
1297
|
+
const valid = validateEngineSession(engineName, session.id, session.cwd);
|
|
977
1298
|
if (!valid) {
|
|
978
1299
|
log('WARN', `${engineName} session ${session.id.slice(0, 8)} invalid for ${sessionChatId}; starting fresh ${engineName} session`);
|
|
979
1300
|
if (!isVirtualAgent) {
|
|
980
1301
|
await bot.sendMessage(chatId, '⚠️ 上次 session 已失效,已自动开启新 session。').catch(() => { });
|
|
981
1302
|
}
|
|
982
|
-
session = createSession(
|
|
1303
|
+
session = createSession(
|
|
1304
|
+
sessionChatId,
|
|
1305
|
+
session.cwd,
|
|
1306
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1307
|
+
engineName,
|
|
1308
|
+
engineName === 'codex' ? requestedCodexPermissionProfile : undefined
|
|
1309
|
+
);
|
|
983
1310
|
}
|
|
984
1311
|
}
|
|
985
1312
|
|
|
986
|
-
|
|
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
|
+
}
|
|
987
1335
|
const mentorCfg = (daemonCfg.mentor && typeof daemonCfg.mentor === 'object') ? daemonCfg.mentor : {};
|
|
988
1336
|
const mentorEnabled = !!(mentorEngine && mentorCfg.enabled);
|
|
989
1337
|
const excludeAgents = new Set(
|
|
@@ -1011,35 +1359,21 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1011
1359
|
}
|
|
1012
1360
|
|
|
1013
1361
|
// Build engine command — prefer per-engine model, fall back to legacy daemon.model
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (engineName === 'codex' && session.cwd && !session.started) {
|
|
1028
|
-
try {
|
|
1029
|
-
const parts = [];
|
|
1030
|
-
const claudeMd = path.join(session.cwd, 'CLAUDE.md');
|
|
1031
|
-
const soulMd = path.join(session.cwd, 'SOUL.md');
|
|
1032
|
-
if (fs.existsSync(claudeMd)) parts.push(fs.readFileSync(claudeMd, 'utf8').trim());
|
|
1033
|
-
if (fs.existsSync(soulMd)) {
|
|
1034
|
-
const soulContent = fs.readFileSync(soulMd, 'utf8').trim();
|
|
1035
|
-
if (soulContent) parts.push(soulContent);
|
|
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})`);
|
|
1036
1375
|
}
|
|
1037
|
-
|
|
1038
|
-
fs.writeFileSync(path.join(session.cwd, 'AGENTS.md'), parts.join('\n\n'), 'utf8');
|
|
1039
|
-
log('INFO', `Refreshed AGENTS.md (${parts.length} section(s)) in ${session.cwd}`);
|
|
1040
|
-
}
|
|
1041
|
-
} catch (e) {
|
|
1042
|
-
log('WARN', `AGENTS.md refresh failed: ${e.message}`);
|
|
1376
|
+
model = resumeInspection.modelPin;
|
|
1043
1377
|
}
|
|
1044
1378
|
}
|
|
1045
1379
|
|
|
@@ -1060,9 +1394,29 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1060
1394
|
|
|
1061
1395
|
// Memory & Knowledge Injection (RAG)
|
|
1062
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 */ }
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1063
1413
|
// projectKey must be declared outside the try block so the daemonHint template below can reference it.
|
|
1064
1414
|
const _cid0 = String(chatId);
|
|
1065
|
-
const _agentMap0 = {
|
|
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
|
+
};
|
|
1066
1420
|
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1067
1421
|
try {
|
|
1068
1422
|
const memory = require('./memory');
|
|
@@ -1159,30 +1513,24 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1159
1513
|
}
|
|
1160
1514
|
|
|
1161
1515
|
// Inject daemon hints only on first message of a session
|
|
1162
|
-
// Task-specific rules (3-
|
|
1516
|
+
// Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
1163
1517
|
let daemonHint = '';
|
|
1164
1518
|
if (!session.started) {
|
|
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
|
+
: '';
|
|
1165
1522
|
const taskRules = isTaskIntent(prompt) ? `
|
|
1166
|
-
3.
|
|
1167
|
-
node ~/.metame/memory-search.js "关键词1" "keyword2"
|
|
1168
|
-
If no relevant facts surface, check ~/.metame/memory/INDEX.md for available playbook/decision docs.
|
|
1169
|
-
Use these before answering complex questions about MetaMe architecture or past decisions.
|
|
1170
|
-
4. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
1523
|
+
3. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
1171
1524
|
node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
|
|
1172
1525
|
Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
|
|
1173
1526
|
Only write verified facts. Do not write speculative or process-description entries.
|
|
1174
|
-
|
|
1175
|
-
|
|
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:
|
|
1176
1529
|
\`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
|
|
1177
1530
|
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
1178
1531
|
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
1179
1532
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
1180
|
-
2.
|
|
1181
|
-
- Just FIND the file path (use Glob/ls if needed)
|
|
1182
|
-
- Do NOT read or summarize the file content (wastes tokens)
|
|
1183
|
-
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
1184
|
-
- Keep response brief: "请查收~! [[FILE:/path/to/file]]"
|
|
1185
|
-
- Multiple files: use multiple [[FILE:...]] tags${zdpHint ? '\n Explanation depth (ZPD):\n' + zdpHint : ''}${taskRules}]`;
|
|
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}]`;
|
|
1186
1534
|
}
|
|
1187
1535
|
|
|
1188
1536
|
daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
|
|
@@ -1279,14 +1627,69 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1279
1627
|
const langGuard = session.started
|
|
1280
1628
|
? ''
|
|
1281
1629
|
: '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
1282
|
-
|
|
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}`);
|
|
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
|
+
}
|
|
1650
|
+
|
|
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);
|
|
1672
|
+
}
|
|
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}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1283
1681
|
|
|
1284
1682
|
// Git checkpoint before Claude modifies files (for /undo).
|
|
1285
1683
|
// Skip for virtual agents (team clones like _agent_yi) — each has its own worktree,
|
|
1286
1684
|
// but checkpoint uses `git add -A` which could interfere with parallel work.
|
|
1287
1685
|
const _isVirtualAgent = String(chatId).startsWith('_agent_') || String(chatId).startsWith('_scope_');
|
|
1288
1686
|
if (!_isVirtualAgent) {
|
|
1289
|
-
|
|
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 */ }
|
|
1290
1693
|
}
|
|
1291
1694
|
log('INFO', `[TIMING:${chatId}] pre-spawn +${Date.now() - _t0}ms (engine:${runtime.name} started:${session.started})`);
|
|
1292
1695
|
|
|
@@ -1352,11 +1755,21 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1352
1755
|
...session,
|
|
1353
1756
|
id: safeNextId,
|
|
1354
1757
|
engine: runtime.name,
|
|
1758
|
+
logicalChatId: sessionChatId,
|
|
1355
1759
|
started: true,
|
|
1356
1760
|
};
|
|
1357
1761
|
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1358
1762
|
const engines = { ...(cur.engines || {}) };
|
|
1359
|
-
|
|
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
|
+
};
|
|
1360
1773
|
return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
|
|
1361
1774
|
});
|
|
1362
1775
|
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
@@ -1364,7 +1777,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1364
1777
|
}
|
|
1365
1778
|
};
|
|
1366
1779
|
|
|
1367
|
-
let output, error, errorCode, files, toolUsageLog, timedOut,
|
|
1780
|
+
let output, error, errorCode, files, toolUsageLog, timedOut, sessionId;
|
|
1368
1781
|
try {
|
|
1369
1782
|
({
|
|
1370
1783
|
output,
|
|
@@ -1373,7 +1786,6 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1373
1786
|
timedOut,
|
|
1374
1787
|
files,
|
|
1375
1788
|
toolUsageLog,
|
|
1376
|
-
usage,
|
|
1377
1789
|
sessionId,
|
|
1378
1790
|
} = await spawnClaudeStreaming(
|
|
1379
1791
|
args,
|
|
@@ -1383,39 +1795,122 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1383
1795
|
600000,
|
|
1384
1796
|
chatId,
|
|
1385
1797
|
boundProjectKey || '',
|
|
1798
|
+
normalizeSenderId(senderId),
|
|
1386
1799
|
runtime,
|
|
1387
1800
|
onSession,
|
|
1388
1801
|
));
|
|
1389
1802
|
|
|
1390
1803
|
if (sessionId) await onSession(sessionId);
|
|
1391
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);
|
|
1392
1881
|
if (shouldRetryCodexResumeFallback({
|
|
1393
1882
|
runtimeName: runtime.name,
|
|
1394
1883
|
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1395
1884
|
output,
|
|
1396
1885
|
error,
|
|
1397
1886
|
errorCode,
|
|
1398
|
-
|
|
1887
|
+
failureKind: resumeFailure.kind,
|
|
1888
|
+
canRetry: canRetryCodexResume(chatId, resumeFailure.kind),
|
|
1399
1889
|
})) {
|
|
1400
|
-
markCodexResumeRetried(chatId);
|
|
1401
|
-
log(
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
session = createSession(
|
|
1405
|
-
sessionChatId,
|
|
1406
|
-
session.cwd,
|
|
1407
|
-
boundProject && boundProject.name ? boundProject.name : '',
|
|
1408
|
-
'codex'
|
|
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)}`
|
|
1409
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
|
+
}
|
|
1410
1905
|
const retryArgs = runtime.buildArgs({
|
|
1411
1906
|
model,
|
|
1412
1907
|
readOnly,
|
|
1413
1908
|
daemonCfg,
|
|
1414
1909
|
session,
|
|
1415
1910
|
cwd: session.cwd,
|
|
1911
|
+
permissionProfile: requestedCodexPermissionProfile,
|
|
1416
1912
|
});
|
|
1417
|
-
|
|
1418
|
-
const retryPrompt = `[Note: previous Codex session expired and could not be resumed. Treating this as a new session. User message follows:]\n\n${fullPrompt}`;
|
|
1913
|
+
const retryPrompt = `${resumeFailure.retryPromptPrefix}\n\n${fullPrompt}`;
|
|
1419
1914
|
({
|
|
1420
1915
|
output,
|
|
1421
1916
|
error,
|
|
@@ -1423,7 +1918,6 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1423
1918
|
timedOut,
|
|
1424
1919
|
files,
|
|
1425
1920
|
toolUsageLog,
|
|
1426
|
-
usage,
|
|
1427
1921
|
sessionId,
|
|
1428
1922
|
} = await spawnClaudeStreaming(
|
|
1429
1923
|
retryArgs,
|
|
@@ -1433,6 +1927,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1433
1927
|
600000,
|
|
1434
1928
|
chatId,
|
|
1435
1929
|
boundProjectKey || '',
|
|
1930
|
+
normalizeSenderId(senderId),
|
|
1436
1931
|
runtime,
|
|
1437
1932
|
onSession,
|
|
1438
1933
|
));
|
|
@@ -1507,7 +2002,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1507
2002
|
}
|
|
1508
2003
|
|
|
1509
2004
|
if (output) {
|
|
1510
|
-
if (runtime.name === 'codex')
|
|
2005
|
+
if (runtime.name === 'codex') {
|
|
2006
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'interrupted'));
|
|
2007
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'expired'));
|
|
2008
|
+
_codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'default'));
|
|
2009
|
+
}
|
|
1511
2010
|
// Detect provider/model errors disguised as output (e.g., "model not found", API errors)
|
|
1512
2011
|
if (runtime.name === 'claude') {
|
|
1513
2012
|
const activeProvCheck = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
@@ -1527,7 +2026,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1527
2026
|
|
|
1528
2027
|
// Mark session as started after first successful call
|
|
1529
2028
|
const wasNew = !session.started;
|
|
1530
|
-
if (wasNew)
|
|
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
|
+
}
|
|
1531
2035
|
|
|
1532
2036
|
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
1533
2037
|
const chatCategory = classifyChatUsage(chatId, {
|
|
@@ -1562,6 +2066,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1562
2066
|
try {
|
|
1563
2067
|
log('DEBUG', `[REPLY:${chatId}] statusMsgId=${statusMsgId} editFailed=${editFailed} activeProject=${activeProject && activeProject.name} lastCard=${_lastStatusCardContent ? _lastStatusCardContent.slice(0, 40) : 'null'}`);
|
|
1564
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
|
+
|
|
1565
2074
|
// Strategy: always try to update the status card first (avoids sending a new card
|
|
1566
2075
|
// while the old 🤔 card lingers, which would produce two messages).
|
|
1567
2076
|
// If edit fails: try to delete the status card (awaited, not fire-and-forget).
|
|
@@ -1613,9 +2122,26 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1613
2122
|
log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
|
|
1614
2123
|
}
|
|
1615
2124
|
}
|
|
1616
|
-
|
|
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
|
+
}
|
|
2132
|
+
}
|
|
1617
2133
|
|
|
1618
|
-
await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
|
|
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
|
+
}
|
|
1619
2145
|
|
|
1620
2146
|
// Timeout: also send the reason after the partial result
|
|
1621
2147
|
if (timedOut && error) {
|
|
@@ -1653,9 +2179,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1653
2179
|
: `Error: ${errMsg.slice(0, 200)}`;
|
|
1654
2180
|
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
1655
2181
|
|
|
1656
|
-
// If session not found
|
|
1657
|
-
|
|
1658
|
-
|
|
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`);
|
|
1659
2188
|
session = createSession(sessionChatId, session.cwd, '', runtime.name);
|
|
1660
2189
|
|
|
1661
2190
|
const retryArgs = runtime.buildArgs({
|
|
@@ -1674,6 +2203,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1674
2203
|
600000,
|
|
1675
2204
|
chatId,
|
|
1676
2205
|
boundProjectKey || '',
|
|
2206
|
+
normalizeSenderId(senderId),
|
|
1677
2207
|
runtime,
|
|
1678
2208
|
onSession,
|
|
1679
2209
|
);
|
|
@@ -1686,7 +2216,10 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1686
2216
|
return { ok: true };
|
|
1687
2217
|
} else {
|
|
1688
2218
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
1689
|
-
|
|
2219
|
+
const retryUserMsg = _isThinkingSignatureError
|
|
2220
|
+
? formatClaudeResumeFallbackUserMessage(retry.error || errMsg)
|
|
2221
|
+
: userErrMsg;
|
|
2222
|
+
try { await bot.sendMessage(chatId, retryUserMsg); } catch { /* */ }
|
|
1690
2223
|
return { ok: false, error: retry.error || errMsg };
|
|
1691
2224
|
}
|
|
1692
2225
|
} else {
|
|
@@ -1733,9 +2266,23 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1733
2266
|
shouldRetryCodexResumeFallback,
|
|
1734
2267
|
formatEngineSpawnError,
|
|
1735
2268
|
adaptDaemonHintForEngine,
|
|
2269
|
+
getSessionChatId,
|
|
2270
|
+
getCodexPermissionProfile,
|
|
2271
|
+
getActualCodexPermissionProfile,
|
|
2272
|
+
sameCodexPermissionProfile,
|
|
2273
|
+
inspectClaudeResumeSession,
|
|
2274
|
+
isClaudeThinkingSignatureError,
|
|
2275
|
+
formatClaudeResumeFallbackUserMessage,
|
|
2276
|
+
classifyCodexResumeFailure,
|
|
1736
2277
|
canRetryCodexResume,
|
|
1737
2278
|
markCodexResumeRetried,
|
|
2279
|
+
getCodexResumeRetryKey,
|
|
1738
2280
|
CODEX_RESUME_RETRY_WINDOW_MS,
|
|
2281
|
+
shouldAutoRouteSkill,
|
|
2282
|
+
codexSandboxPrivilegeRank,
|
|
2283
|
+
codexApprovalPrivilegeRank,
|
|
2284
|
+
codexNeedsFallbackForRequestedPermissions,
|
|
2285
|
+
buildCodexFallbackBridgePrompt,
|
|
1739
2286
|
},
|
|
1740
2287
|
};
|
|
1741
2288
|
}
|