metame-cli 1.5.2 → 1.5.4
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 +90 -17
- package/index.js +76 -25
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +167 -90
- package/scripts/daemon-admin-commands.js +225 -24
- package/scripts/daemon-agent-commands.js +263 -8
- package/scripts/daemon-bridges.js +395 -6
- package/scripts/daemon-claude-engine.js +749 -582
- package/scripts/daemon-command-router.js +104 -0
- package/scripts/daemon-default.yaml +9 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +8 -5
- package/scripts/daemon-file-browser.js +1 -0
- package/scripts/daemon-remote-dispatch.js +82 -0
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +19 -11
- package/scripts/daemon-session-store.js +26 -8
- package/scripts/daemon-task-scheduler.js +2 -2
- package/scripts/daemon.js +363 -8
- package/scripts/daemon.yaml +356 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +131 -0
- package/scripts/docs/maintenance-manual.md +214 -3
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-agent-manage.js +50 -0
- package/scripts/hooks/intent-engine.js +103 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-hook-config.js +28 -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/memory-extract.js +1 -1
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory-write.js +21 -4
- package/scripts/memory.js +55 -17
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
- package/scripts/team-dispatch.js +176 -0
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
const { classifyChatUsage } = require('./usage-classifier');
|
|
4
4
|
const { deriveProjectInfo } = require('./utils');
|
|
5
|
-
const { createEngineRuntimeFactory, normalizeEngineName, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
5
|
+
const { createEngineRuntimeFactory, normalizeEngineName, resolveEngineModel, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
6
6
|
const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Antigravity Raw Session Logging — Lossless Diary (L0)
|
|
10
|
+
* [PROTECTED] Append every user→AI turn to a daily markdown file.
|
|
11
|
+
* Isolated as a standalone function to prevent accidental deletion during edits.
|
|
12
|
+
*/
|
|
13
|
+
function logRawSessionDiary(fs, path, HOME, { chatId, prompt, output, error, projectKey }) {
|
|
14
|
+
try {
|
|
15
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
16
|
+
const ym = today.slice(0, 7); // YYYY-MM
|
|
17
|
+
const sessDir = path.join(HOME, '.metame', 'sessions', ym);
|
|
18
|
+
if (!fs.existsSync(sessDir)) fs.mkdirSync(sessDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const diaryPath = path.join(sessDir, `${today}_${chatId}.md`);
|
|
21
|
+
const MAX_OUTPUT_LOG = 8000;
|
|
22
|
+
const outputLog = (output || error || 'No output.').slice(0, MAX_OUTPUT_LOG);
|
|
23
|
+
const outputTruncated = (output || '').length > MAX_OUTPUT_LOG ? '\n\n[truncated]' : '';
|
|
24
|
+
const entry = `\n---\ndate: ${new Date().toISOString()}\nproject: ${projectKey || 'global'}\n---\n\n## 🙋♂️ 用户指令\n\`\`\`text\n${prompt}\n\`\`\`\n\n## 🤖 执行实录\n${outputLog}${outputTruncated}\n`;
|
|
25
|
+
fs.appendFileSync(diaryPath, entry, 'utf8');
|
|
26
|
+
} catch (e) { console.warn(`[MetaMe] Raw session logging failed: ${e.message}`); }
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
function createClaudeEngine(deps) {
|
|
9
30
|
const {
|
|
10
31
|
fs,
|
|
@@ -457,6 +478,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
457
478
|
WebFetch: '🌐',
|
|
458
479
|
WebSearch: '🔍',
|
|
459
480
|
Task: '🤖',
|
|
481
|
+
Agent: '🤖',
|
|
460
482
|
Skill: '🔧',
|
|
461
483
|
TodoWrite: '📋',
|
|
462
484
|
NotebookEdit: '📓',
|
|
@@ -520,6 +542,17 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
520
542
|
let classifiedError = null;
|
|
521
543
|
let lastStatusTime = 0;
|
|
522
544
|
const STATUS_THROTTLE = statusThrottleMs;
|
|
545
|
+
// Streaming card: accumulate text and push to card in real-time (throttled)
|
|
546
|
+
let _streamText = '';
|
|
547
|
+
let _lastStreamFlush = 0;
|
|
548
|
+
const STREAM_THROTTLE = 1500; // ms between card edits (safe within Feishu 5 req/s limit)
|
|
549
|
+
function flushStream(force) {
|
|
550
|
+
if (!onStatus || !_streamText.trim()) return;
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
if (!force && now - _lastStreamFlush < STREAM_THROTTLE) return;
|
|
553
|
+
_lastStreamFlush = now;
|
|
554
|
+
onStatus('__STREAM_TEXT__' + _streamText).catch(() => { });
|
|
555
|
+
}
|
|
523
556
|
const writtenFiles = [];
|
|
524
557
|
const toolUsageLog = [];
|
|
525
558
|
|
|
@@ -568,7 +601,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
568
601
|
const ctx = recentTool.context || recentTool.skill || '';
|
|
569
602
|
parts.push(`最近: ${recentTool.tool}${ctx ? ' ' + ctx : ''}`);
|
|
570
603
|
}
|
|
571
|
-
if (onStatus)
|
|
604
|
+
if (onStatus) {
|
|
605
|
+
const milestoneMsg = parts.join(' | ');
|
|
606
|
+
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${milestoneMsg}` : milestoneMsg;
|
|
607
|
+
onStatus(msg).catch(() => { });
|
|
608
|
+
}
|
|
572
609
|
}
|
|
573
610
|
}, 30000);
|
|
574
611
|
|
|
@@ -607,11 +644,13 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
607
644
|
continue;
|
|
608
645
|
}
|
|
609
646
|
if (event.type === 'text' && event.text) {
|
|
610
|
-
finalResult
|
|
647
|
+
finalResult += (finalResult ? '\n\n' : '') + String(event.text);
|
|
648
|
+
_streamText = finalResult;
|
|
611
649
|
if (waitingForTool) {
|
|
612
650
|
waitingForTool = false;
|
|
613
651
|
resetIdleTimer();
|
|
614
652
|
}
|
|
653
|
+
flushStream(); // throttled stream to card
|
|
615
654
|
continue;
|
|
616
655
|
}
|
|
617
656
|
if (event.type === 'done') {
|
|
@@ -620,6 +659,13 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
620
659
|
waitingForTool = false;
|
|
621
660
|
resetIdleTimer();
|
|
622
661
|
}
|
|
662
|
+
// Fallback: if no text streamed yet (tool-only response), use result text from done.
|
|
663
|
+
// Do NOT use when finalResult already has content — result duplicates streamed text.
|
|
664
|
+
if (!finalResult && event.result) {
|
|
665
|
+
finalResult = String(event.result);
|
|
666
|
+
_streamText = finalResult;
|
|
667
|
+
}
|
|
668
|
+
flushStream(true); // force final text flush before process ends
|
|
623
669
|
continue;
|
|
624
670
|
}
|
|
625
671
|
if (event.type === 'tool_result') {
|
|
@@ -659,8 +705,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
659
705
|
|
|
660
706
|
if (toolName === 'Skill' && toolInput.skill) {
|
|
661
707
|
context = toolInput.skill;
|
|
662
|
-
} else if (toolName === 'Task' && toolInput.description) {
|
|
663
|
-
|
|
708
|
+
} else if ((toolName === 'Task' || toolName === 'Agent') && toolInput.description) {
|
|
709
|
+
const agentType = toolInput.subagent_type ? `[${toolInput.subagent_type}] ` : '';
|
|
710
|
+
context = (agentType + String(toolInput.description)).slice(0, 40);
|
|
664
711
|
} else if (toolName.startsWith('mcp__')) {
|
|
665
712
|
const parts = toolName.split('__');
|
|
666
713
|
const server = parts[1] || 'unknown';
|
|
@@ -692,7 +739,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
692
739
|
const status = context
|
|
693
740
|
? `${displayEmoji} ${displayName}: 「${context}」`
|
|
694
741
|
: `${displayEmoji} ${displayName}...`;
|
|
695
|
-
if (onStatus)
|
|
742
|
+
if (onStatus) {
|
|
743
|
+
// Overlay tool status on top of streamed text (if any); else show plain status
|
|
744
|
+
const msg = _streamText ? `__TOOL_OVERLAY__${_streamText}\n\n> ${status}` : status;
|
|
745
|
+
onStatus(msg).catch(() => { });
|
|
746
|
+
}
|
|
696
747
|
}
|
|
697
748
|
}
|
|
698
749
|
});
|
|
@@ -782,11 +833,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
782
833
|
|
|
783
834
|
// Track outbound message_id → session for reply-based session restoration.
|
|
784
835
|
// Keeps last 200 entries to avoid unbounded growth.
|
|
785
|
-
function trackMsgSession(messageId, session) {
|
|
836
|
+
function trackMsgSession(messageId, session, agentKey) {
|
|
786
837
|
if (!messageId || !session || !session.id) return;
|
|
787
838
|
const st = loadState();
|
|
788
839
|
if (!st.msg_sessions) st.msg_sessions = {};
|
|
789
|
-
st.msg_sessions[messageId] = { id: session.id, cwd: session.cwd, engine: session.engine || getDefaultEngine() };
|
|
840
|
+
st.msg_sessions[messageId] = { id: session.id, cwd: session.cwd, engine: session.engine || getDefaultEngine(), agentKey: agentKey || null };
|
|
790
841
|
const keys = Object.keys(st.msg_sessions);
|
|
791
842
|
if (keys.length > 200) {
|
|
792
843
|
for (const k of keys.slice(0, keys.length - 200)) delete st.msg_sessions[k];
|
|
@@ -831,9 +882,23 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
831
882
|
// Send 🤔 ack and start typing — fire-and-forget so we don't block spawn on Telegram RTT.
|
|
832
883
|
// statusMsgId is resolved via a promise; it will be ready well before the first model output.
|
|
833
884
|
let statusMsgId = null;
|
|
885
|
+
let _lastStatusCardContent = null; // tracks last clean text written to card (for final-reply dedup)
|
|
886
|
+
// Early detect bound project for branded ack card (team members / dispatch agents)
|
|
887
|
+
const _ackChatIdStr = String(chatId);
|
|
888
|
+
const _ackAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map || {} : {}), ...(config.feishu ? config.feishu.chat_agent_map || {} : {}) };
|
|
889
|
+
const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
|
|
890
|
+
const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
|
|
891
|
+
// _ackCardHeader: non-null for agents with icon/name (team members, dispatch); passed to editMessage to preserve header on streaming edits
|
|
892
|
+
const _ackCardHeader = (_ackBoundProj && _ackBoundProj.icon && _ackBoundProj.name)
|
|
893
|
+
? { title: `${_ackBoundProj.icon} ${_ackBoundProj.name}`, color: _ackBoundProj.color || 'blue' }
|
|
894
|
+
: null;
|
|
834
895
|
// Fire-and-forget: don't await Telegram RTT before spawning the engine process.
|
|
835
896
|
// statusMsgId will be populated well before the first model output (~5s for codex).
|
|
836
|
-
|
|
897
|
+
// For branded agents: send a card with header so streaming edits preserve the agent identity.
|
|
898
|
+
const _ackFn = (_ackCardHeader && bot.sendCard)
|
|
899
|
+
? () => bot.sendCard(chatId, { title: _ackCardHeader.title, body: '🤔', color: _ackCardHeader.color })
|
|
900
|
+
: () => (bot.sendMarkdown ? bot.sendMarkdown(chatId, '🤔') : bot.sendMessage(chatId, '🤔'));
|
|
901
|
+
_ackFn()
|
|
837
902
|
.then(msg => { if (msg && msg.message_id) statusMsgId = msg.message_id; })
|
|
838
903
|
.catch(e => log('ERROR', `Failed to send ack to ${chatId}: ${e.message}`));
|
|
839
904
|
bot.sendTyping(chatId).catch(() => { });
|
|
@@ -846,234 +911,261 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
846
911
|
// kill the handler, leaving the typing indicator spinning forever.
|
|
847
912
|
try { // ── safety-net-start ──
|
|
848
913
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
914
|
+
// Agent nickname routing: "贾维斯" / "小美,帮我..." → switch project session
|
|
915
|
+
// Strict chats (chat_agent_map bound groups) must NOT switch agents via nickname
|
|
916
|
+
const _strictAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
917
|
+
const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
|
|
918
|
+
const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
|
|
919
|
+
if (agentMatch) {
|
|
920
|
+
const { key, proj, rest } = agentMatch;
|
|
921
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
922
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key, proj.engine ? normalizeEngineName(proj.engine) : getDefaultEngine());
|
|
923
|
+
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
924
|
+
if (!rest) {
|
|
925
|
+
// Pure nickname call — confirm switch and stop
|
|
926
|
+
clearInterval(typingTimer);
|
|
927
|
+
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
928
|
+
return { ok: true };
|
|
929
|
+
}
|
|
930
|
+
// Nickname + content — strip nickname, continue with rest as prompt
|
|
931
|
+
prompt = rest;
|
|
864
932
|
}
|
|
865
|
-
// Nickname + content — strip nickname, continue with rest as prompt
|
|
866
|
-
prompt = rest;
|
|
867
|
-
}
|
|
868
933
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
// Note: daemon_state.json persists across restarts, so this only happens on truly first use
|
|
895
|
-
// or after an explicit /new command.
|
|
896
|
-
createSession(chatId, boundCwd || undefined, boundProject && boundProject.name ? boundProject.name : '', boundEngineName);
|
|
897
|
-
}
|
|
934
|
+
// Skill routing: detect skill first, then decide session
|
|
935
|
+
// BUT: skip skill routing if agent addressed by nickname OR chat already has an active session
|
|
936
|
+
// (active conversation should never be hijacked by keyword-based skill matching)
|
|
937
|
+
const chatIdStr = String(chatId);
|
|
938
|
+
const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
939
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
940
|
+
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
941
|
+
// Each virtual chatId (including clones) keeps its own isolated session.
|
|
942
|
+
// Parallel tasks must not share JSONL files — concurrent writes cause corruption.
|
|
943
|
+
const sessionChatId = boundProjectKey ? `_agent_${boundProjectKey}` : chatId;
|
|
944
|
+
const sessionRaw = getSession(sessionChatId);
|
|
945
|
+
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
946
|
+
const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
|
|
947
|
+
|
|
948
|
+
// Engine is determined from config only — bound agent config wins, then global default.
|
|
949
|
+
const engineName = normalizeEngineName(
|
|
950
|
+
(boundProject && boundProject.engine) || getDefaultEngine()
|
|
951
|
+
);
|
|
952
|
+
const runtime = getEngineRuntime(engineName);
|
|
953
|
+
|
|
954
|
+
// hasActiveSession: does the current engine have an ongoing conversation?
|
|
955
|
+
const hasActiveSession = sessionRaw && (
|
|
956
|
+
sessionRaw.engines ? !!(sessionRaw.engines[engineName]?.started) : !!sessionRaw.started
|
|
957
|
+
);
|
|
958
|
+
const skill = (agentMatch || hasActiveSession) ? null : routeSkill(prompt);
|
|
898
959
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
// Claude checks JSONL file existence; Codex checks SQLite. Same interface, different backend.
|
|
905
|
-
if (session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
906
|
-
const valid = isEngineSessionValid(engineName, session.id, session.cwd);
|
|
907
|
-
if (!valid) {
|
|
908
|
-
log('WARN', `${engineName} session ${session.id.slice(0, 8)} invalid for ${chatId}; starting fresh ${engineName} session`);
|
|
909
|
-
await bot.sendMessage(chatId, '⚠️ 上次 session 已失效,已自动开启新 session。').catch(() => {});
|
|
910
|
-
session = createSession(chatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', engineName);
|
|
960
|
+
if (!sessionRaw) {
|
|
961
|
+
// No saved state for this chatId: start a fresh session.
|
|
962
|
+
// Note: daemon_state.json persists across restarts, so this only happens on truly first use
|
|
963
|
+
// or after an explicit /new command.
|
|
964
|
+
createSession(sessionChatId, boundCwd || undefined, boundProject && boundProject.name ? boundProject.name : '', boundEngineName);
|
|
911
965
|
}
|
|
912
|
-
}
|
|
913
966
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
try {
|
|
929
|
-
const breaker = mentorEngine.checkEmotionBreaker(prompt, mentorCfg);
|
|
930
|
-
if (breaker && breaker.tripped) {
|
|
931
|
-
mentorSuppressed = true;
|
|
932
|
-
if (breaker.reason !== 'cooldown_active' && breaker.response) {
|
|
933
|
-
await bot.sendMessage(chatId, breaker.response).catch(() => { });
|
|
967
|
+
// Resolve flat view for current engine (id + started are engine-specific; cwd is shared)
|
|
968
|
+
let session = getSessionForEngine(sessionChatId, engineName) || { cwd: boundCwd || HOME, engine: engineName, id: null, started: false };
|
|
969
|
+
session.engine = engineName; // keep local copy for Codex resume detection below
|
|
970
|
+
|
|
971
|
+
// Pre-spawn session validation: unified for all engines.
|
|
972
|
+
// Claude checks JSONL file existence; Codex checks SQLite. Same interface, different backend.
|
|
973
|
+
// Skip warning for virtual agents (team members) - they may use worktrees with fresh sessions
|
|
974
|
+
const isVirtualAgent = String(sessionChatId).startsWith('_agent_');
|
|
975
|
+
if (session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
976
|
+
const valid = isEngineSessionValid(engineName, session.id, session.cwd);
|
|
977
|
+
if (!valid) {
|
|
978
|
+
log('WARN', `${engineName} session ${session.id.slice(0, 8)} invalid for ${sessionChatId}; starting fresh ${engineName} session`);
|
|
979
|
+
if (!isVirtualAgent) {
|
|
980
|
+
await bot.sendMessage(chatId, '⚠️ 上次 session 已失效,已自动开启新 session。').catch(() => { });
|
|
934
981
|
}
|
|
982
|
+
session = createSession(sessionChatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', engineName);
|
|
935
983
|
}
|
|
936
|
-
} catch (e) {
|
|
937
|
-
log('WARN', `Mentor breaker failed: ${e.message}`);
|
|
938
984
|
}
|
|
939
|
-
}
|
|
940
985
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
986
|
+
const daemonCfg = (config && config.daemon) || {};
|
|
987
|
+
const mentorCfg = (daemonCfg.mentor && typeof daemonCfg.mentor === 'object') ? daemonCfg.mentor : {};
|
|
988
|
+
const mentorEnabled = !!(mentorEngine && mentorCfg.enabled);
|
|
989
|
+
const excludeAgents = new Set(
|
|
990
|
+
(Array.isArray(mentorCfg.exclude_agents) ? mentorCfg.exclude_agents : [])
|
|
991
|
+
.map(x => String(x || '').trim())
|
|
992
|
+
.filter(Boolean)
|
|
993
|
+
);
|
|
994
|
+
const chatAgentKey = boundProjectKey || 'personal';
|
|
995
|
+
const mentorExcluded = excludeAgents.has(chatAgentKey);
|
|
996
|
+
let mentorSuppressed = false;
|
|
952
997
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (soulContent) parts.push(soulContent);
|
|
966
|
-
}
|
|
967
|
-
if (parts.length > 0) {
|
|
968
|
-
fs.writeFileSync(path.join(session.cwd, 'AGENTS.md'), parts.join('\n\n'), 'utf8');
|
|
969
|
-
log('INFO', `Refreshed AGENTS.md (${parts.length} section(s)) in ${session.cwd}`);
|
|
998
|
+
// Mentor pre-flight breaker: first hit sends a short reassurance; cooldown does not block normal answers.
|
|
999
|
+
if (mentorEnabled && !mentorExcluded) {
|
|
1000
|
+
try {
|
|
1001
|
+
const breaker = mentorEngine.checkEmotionBreaker(prompt, mentorCfg);
|
|
1002
|
+
if (breaker && breaker.tripped) {
|
|
1003
|
+
mentorSuppressed = true;
|
|
1004
|
+
if (breaker.reason !== 'cooldown_active' && breaker.response) {
|
|
1005
|
+
await bot.sendMessage(chatId, breaker.response).catch(() => { });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
log('WARN', `Mentor breaker failed: ${e.message}`);
|
|
970
1010
|
}
|
|
971
|
-
} catch (e) {
|
|
972
|
-
log('WARN', `AGENTS.md refresh failed: ${e.message}`);
|
|
973
1011
|
}
|
|
974
|
-
}
|
|
975
1012
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
).hint || '';
|
|
986
|
-
} catch (e) {
|
|
987
|
-
log('WARN', `Agent context injection failed: ${e.message}`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Memory & Knowledge Injection (RAG)
|
|
992
|
-
let memoryHint = '';
|
|
993
|
-
// projectKey must be declared outside the try block so the daemonHint template below can reference it.
|
|
994
|
-
const _cid0 = String(chatId);
|
|
995
|
-
const _agentMap0 = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
996
|
-
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
997
|
-
try {
|
|
998
|
-
const memory = require('./memory');
|
|
1013
|
+
// Build engine command — prefer per-engine model, fall back to legacy daemon.model
|
|
1014
|
+
const model = resolveEngineModel(runtime.name, daemonCfg, boundProject && boundProject.model);
|
|
1015
|
+
const args = runtime.buildArgs({
|
|
1016
|
+
model,
|
|
1017
|
+
readOnly,
|
|
1018
|
+
daemonCfg,
|
|
1019
|
+
session,
|
|
1020
|
+
cwd: session.cwd,
|
|
1021
|
+
});
|
|
999
1022
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1023
|
+
// Codex: write/refresh AGENTS.md = CLAUDE.md + SOUL.md on every new session.
|
|
1024
|
+
// Written as a real file (not a symlink) for Windows compatibility.
|
|
1025
|
+
// Refreshed each session so edits to CLAUDE.md or SOUL.md are always picked up.
|
|
1026
|
+
// Codex auto-loads AGENTS.md from cwd and all parent dirs up to ~.
|
|
1027
|
+
if (engineName === 'codex' && session.cwd && !session.started) {
|
|
1002
1028
|
try {
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
if (fs.existsSync(
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
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);
|
|
1011
1036
|
}
|
|
1012
|
-
|
|
1037
|
+
if (parts.length > 0) {
|
|
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}`);
|
|
1043
|
+
}
|
|
1013
1044
|
}
|
|
1014
1045
|
|
|
1015
|
-
|
|
1016
|
-
if (!session.started) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1046
|
+
let agentHint = '';
|
|
1047
|
+
if (!session.started && (boundProject || (session && session.cwd))) {
|
|
1048
|
+
try {
|
|
1049
|
+
// Engine-aware: Codex gets memory only (soul is already in AGENTS.md);
|
|
1050
|
+
// Claude gets soul + memory (SOUL.md is not auto-loaded by Claude).
|
|
1051
|
+
agentHint = buildAgentContextForEngine(
|
|
1052
|
+
boundProject || { cwd: session.cwd },
|
|
1053
|
+
engineName,
|
|
1054
|
+
HOME,
|
|
1055
|
+
).hint || '';
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
log('WARN', `Agent context injection failed: ${e.message}`);
|
|
1021
1058
|
}
|
|
1022
1059
|
}
|
|
1023
1060
|
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1061
|
+
// Memory & Knowledge Injection (RAG)
|
|
1062
|
+
let memoryHint = '';
|
|
1063
|
+
// projectKey must be declared outside the try block so the daemonHint template below can reference it.
|
|
1064
|
+
const _cid0 = String(chatId);
|
|
1065
|
+
const _agentMap0 = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
1066
|
+
const projectKey = _agentMap0[_cid0] || projectKeyFromVirtualChatId(_cid0);
|
|
1067
|
+
try {
|
|
1068
|
+
const memory = require('./memory');
|
|
1069
|
+
|
|
1070
|
+
// L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
|
|
1071
|
+
if (!session.started) {
|
|
1072
|
+
try {
|
|
1073
|
+
const nowDir = path.join(HOME, '.metame', 'memory', 'now');
|
|
1074
|
+
const nowKey = projectKey || 'default';
|
|
1075
|
+
const nowPath = path.join(nowDir, `${nowKey}.md`);
|
|
1076
|
+
if (fs.existsSync(nowPath)) {
|
|
1077
|
+
const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
|
|
1078
|
+
if (nowContent) {
|
|
1079
|
+
memoryHint += `\n\n[Current task context:\n${nowContent}]`;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
} catch { /* non-critical */ }
|
|
1035
1083
|
}
|
|
1036
|
-
}
|
|
1037
1084
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1085
|
+
// 1. Inject recent session memories ONLY on first message of a session
|
|
1086
|
+
if (!session.started) {
|
|
1087
|
+
const recent = memory.recentSessions({ limit: 1, project: projectKey || undefined });
|
|
1088
|
+
if (recent.length > 0) {
|
|
1089
|
+
const items = recent.map(r => `- [${r.created_at}] ${r.summary}${r.keywords ? ' (keywords: ' + r.keywords + ')' : ''}`).join('\n');
|
|
1090
|
+
memoryHint += `\n\n[Past session memory:\n${items}]`;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1042
1093
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1094
|
+
// 2. Dynamic Fact Injection (RAG) — first message only
|
|
1095
|
+
// Facts stay in Claude's context for the rest of the session; no need to repeat.
|
|
1096
|
+
// Uses QMD hybrid search if available, falls back to FTS5.
|
|
1097
|
+
if (!session.started) {
|
|
1098
|
+
const searchFn = memory.searchFactsAsync || memory.searchFacts;
|
|
1099
|
+
const factQuery = buildFactSearchQuery(prompt, projectKey);
|
|
1100
|
+
const facts = await Promise.resolve(searchFn(factQuery, { limit: 3, project: projectKey || undefined }));
|
|
1101
|
+
if (facts.length > 0) {
|
|
1102
|
+
// Separate capsule facts from regular facts
|
|
1103
|
+
const capsuleFacts = facts.filter(f => f.relation === 'knowledge_capsule');
|
|
1104
|
+
const regularFacts = facts.filter(f => f.relation !== 'knowledge_capsule');
|
|
1105
|
+
|
|
1106
|
+
// Inject regular facts as before
|
|
1107
|
+
if (regularFacts.length > 0) {
|
|
1108
|
+
const factItems = regularFacts.map(f => `- [${f.relation}] ${f.value}`).join('\n');
|
|
1109
|
+
memoryHint += `\n\n[Relevant facts:\n${factItems}]`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Capsule facts: derive file path from entity and inject as direct "must read" hint
|
|
1113
|
+
// Entity pattern: capsule.metame_daemon_dispatch → capsules/metame-daemon-dispatch-playbook.md
|
|
1114
|
+
if (capsuleFacts.length > 0) {
|
|
1115
|
+
const capsulePaths = capsuleFacts.map(f => {
|
|
1116
|
+
const slug = f.entity.replace(/^capsule\./, '').replace(/_/g, '-');
|
|
1117
|
+
return path.join(HOME, '.metame', 'memory', 'capsules', `${slug}-playbook.md`);
|
|
1118
|
+
}).filter(p => fs.existsSync(p));
|
|
1119
|
+
if (capsulePaths.length > 0) {
|
|
1120
|
+
// Inject file paths only (no shell commands) — works cross-platform and with all engines.
|
|
1121
|
+
// Claude Code reads via Read tool; Codex/Gemini parse the path directly.
|
|
1122
|
+
memoryHint += `\n\n[Relevant playbook detected — read before answering:\n${capsulePaths.map(p => ` ${p}`).join('\n')}]`;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
log('INFO', `[MEMORY] Injected ${regularFacts.length} facts, ${capsuleFacts.length} capsule(s) (query_len=${factQuery.length})`);
|
|
1058
1127
|
}
|
|
1059
1128
|
}
|
|
1060
|
-
} catch { /* non-critical */ }
|
|
1061
|
-
}
|
|
1062
|
-
if (!brainDoc) {
|
|
1063
|
-
try {
|
|
1064
|
-
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1065
|
-
if (fs.existsSync(brainPath)) brainDoc = yaml.load(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1066
|
-
} catch { /* ignore */ }
|
|
1067
|
-
}
|
|
1068
1129
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1130
|
+
memory.close();
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// ZPD: build competence hint from brain profile
|
|
1136
|
+
let zdpHint = '';
|
|
1137
|
+
let brainDoc = null;
|
|
1138
|
+
if (!session.started) {
|
|
1139
|
+
try {
|
|
1140
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1141
|
+
if (fs.existsSync(brainPath)) {
|
|
1142
|
+
const brain = yaml.load(fs.readFileSync(brainPath, 'utf8'));
|
|
1143
|
+
brainDoc = brain;
|
|
1144
|
+
const cmap = brain && brain.user_competence_map;
|
|
1145
|
+
if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
|
|
1146
|
+
const lines = Object.entries(cmap)
|
|
1147
|
+
.map(([domain, level]) => ` ${domain}: ${level}`)
|
|
1148
|
+
.join('\n');
|
|
1149
|
+
zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} catch { /* non-critical */ }
|
|
1153
|
+
}
|
|
1154
|
+
if (!brainDoc) {
|
|
1155
|
+
try {
|
|
1156
|
+
const brainPath = path.join(HOME, '.claude_profile.yaml');
|
|
1157
|
+
if (fs.existsSync(brainPath)) brainDoc = yaml.load(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1158
|
+
} catch { /* ignore */ }
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Inject daemon hints only on first message of a session
|
|
1162
|
+
// Task-specific rules (3-5) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
|
|
1163
|
+
let daemonHint = '';
|
|
1164
|
+
if (!session.started) {
|
|
1165
|
+
const taskRules = isTaskIntent(prompt) ? `
|
|
1074
1166
|
3. Knowledge retrieval: When you need context about a specific topic, past decisions, or lessons, call:
|
|
1075
1167
|
node ~/.metame/memory-search.js "关键词1" "keyword2"
|
|
1076
|
-
|
|
1168
|
+
If no relevant facts surface, check ~/.metame/memory/INDEX.md for available playbook/decision docs.
|
|
1077
1169
|
Use these before answering complex questions about MetaMe architecture or past decisions.
|
|
1078
1170
|
4. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
|
|
1079
1171
|
node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
|
|
@@ -1083,7 +1175,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1083
1175
|
5. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/now/${projectKey || 'default'}.md using:
|
|
1084
1176
|
\`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
|
|
1085
1177
|
Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
|
|
1086
|
-
|
|
1178
|
+
daemonHint = `\n\n[System hints - DO NOT mention these to user:
|
|
1087
1179
|
1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
1088
1180
|
2. File sending: User is on MOBILE. When they ask to see/download a file:
|
|
1089
1181
|
- Just FIND the file path (use Glob/ls if needed)
|
|
@@ -1091,205 +1183,189 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1091
1183
|
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
1092
1184
|
- Keep response brief: "请查收~! [[FILE:/path/to/file]]"
|
|
1093
1185
|
- Multiple files: use multiple [[FILE:...]] tags${zdpHint ? '\n Explanation depth (ZPD):\n' + zdpHint : ''}${taskRules}]`;
|
|
1094
|
-
|
|
1186
|
+
}
|
|
1095
1187
|
|
|
1096
|
-
|
|
1188
|
+
daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
|
|
1097
1189
|
|
|
1098
|
-
|
|
1190
|
+
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
1099
1191
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1192
|
+
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
1193
|
+
// without forcing users to write slash commands by hand.
|
|
1194
|
+
let macAutomationHint = '';
|
|
1195
|
+
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
1196
|
+
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
1105
1197
|
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
1106
1198
|
2. Read/query actions can execute directly.
|
|
1107
1199
|
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.
|
|
1108
1200
|
4. Keep output concise: success/failure + key result only.
|
|
1109
1201
|
5. If permission is missing, guide user to run /mac perms open then retry.
|
|
1110
1202
|
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).]`;
|
|
1111
|
-
|
|
1203
|
+
}
|
|
1112
1204
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1205
|
+
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
1206
|
+
let summaryHint = '';
|
|
1207
|
+
if (session.started) {
|
|
1208
|
+
try {
|
|
1209
|
+
const _stSum = loadState();
|
|
1210
|
+
const _sess = _stSum.sessions && _stSum.sessions[chatId];
|
|
1211
|
+
if (_sess && _sess.last_summary && _sess.last_summary_at) {
|
|
1212
|
+
const _idleMs = Date.now() - (_sess.last_active || 0);
|
|
1213
|
+
const _summaryAgeH = (Date.now() - _sess.last_summary_at) / 3600000;
|
|
1214
|
+
if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
|
|
1215
|
+
summaryHint = `
|
|
1124
1216
|
|
|
1125
1217
|
[上次对话摘要,供参考]: ${_sess.last_summary}`;
|
|
1126
|
-
|
|
1218
|
+
log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
|
|
1219
|
+
}
|
|
1127
1220
|
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1221
|
+
} catch { /* non-critical */ }
|
|
1222
|
+
}
|
|
1131
1223
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1224
|
+
// Mentor context hook: inject after memoryHint, before langGuard.
|
|
1225
|
+
let mentorHint = '';
|
|
1226
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed) {
|
|
1227
|
+
try {
|
|
1228
|
+
const signals = collectRecentSessionSignals(session.id, 6);
|
|
1229
|
+
let skeleton = null;
|
|
1230
|
+
if (sessionAnalytics && typeof sessionAnalytics.extractSkeleton === 'function') {
|
|
1231
|
+
const file = findSessionFile(session.id);
|
|
1232
|
+
if (file && fs.existsSync(file)) {
|
|
1233
|
+
const st = fs.statSync(file);
|
|
1234
|
+
if (st.size <= 2 * 1024 * 1024) {
|
|
1235
|
+
skeleton = sessionAnalytics.extractSkeleton(file);
|
|
1236
|
+
}
|
|
1144
1237
|
}
|
|
1145
1238
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1239
|
+
const zone = skeleton && mentorEngine.computeZone
|
|
1240
|
+
? mentorEngine.computeZone(skeleton).zone
|
|
1241
|
+
: 'stretch';
|
|
1242
|
+
const sessionState = {
|
|
1243
|
+
zone,
|
|
1244
|
+
recentMessages: signals.recentMessages,
|
|
1245
|
+
cwd: session.cwd,
|
|
1246
|
+
skeleton,
|
|
1247
|
+
sessionStartTime: signals.sessionStartTime || new Date().toISOString(),
|
|
1248
|
+
topic: String(prompt || '').slice(0, 120),
|
|
1249
|
+
currentTopic: String(prompt || '').slice(0, 120),
|
|
1250
|
+
lastUserMessage: String(prompt || '').slice(0, 200),
|
|
1251
|
+
};
|
|
1252
|
+
const built = mentorEngine.buildMentorPrompt(sessionState, brainDoc || {}, mentorCfg);
|
|
1253
|
+
if (built && String(built).trim()) mentorHint = `\n\n${String(built).trim()}`;
|
|
1254
|
+
|
|
1255
|
+
// Collect reflection debt: if user returns to same project+topic, inject recall prompt.
|
|
1256
|
+
// Suppressed by quiet_until (user explicitly asked for silence), but NOT by expert skip
|
|
1257
|
+
// (even experts may not have reviewed AI-generated code).
|
|
1258
|
+
const quietUntil = brainDoc && brainDoc.growth ? brainDoc.growth.quiet_until : null;
|
|
1259
|
+
const quietMs = quietUntil ? new Date(quietUntil).getTime() : 0;
|
|
1260
|
+
const isQuiet = quietMs && quietMs > Date.now();
|
|
1261
|
+
if (!isQuiet && mentorEngine.collectDebt) {
|
|
1262
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1263
|
+
const projectId = info && info.project_id ? info.project_id : '';
|
|
1264
|
+
if (projectId) {
|
|
1265
|
+
const debt = mentorEngine.collectDebt(projectId, String(prompt || '').slice(0, 120));
|
|
1266
|
+
if (debt && debt.prompt) {
|
|
1267
|
+
mentorHint += `\n\n[Reflection debt] ${debt.prompt}`;
|
|
1268
|
+
}
|
|
1176
1269
|
}
|
|
1177
1270
|
}
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
log('WARN', `Mentor prompt build failed: ${e.message}`);
|
|
1178
1273
|
}
|
|
1179
|
-
} catch (e) {
|
|
1180
|
-
log('WARN', `Mentor prompt build failed: ${e.message}`);
|
|
1181
1274
|
}
|
|
1182
|
-
}
|
|
1183
1275
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1276
|
+
// Language guard: only inject on first message of a new session to avoid
|
|
1277
|
+
// linearly growing token cost on every turn in long conversations.
|
|
1278
|
+
// Claude Code preserves session context, so the guard persists after initial injection.
|
|
1279
|
+
const langGuard = session.started
|
|
1280
|
+
? ''
|
|
1281
|
+
: '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
|
|
1282
|
+
const fullPrompt = routedPrompt + daemonHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
|
|
1283
|
+
|
|
1284
|
+
// Git checkpoint before Claude modifies files (for /undo).
|
|
1285
|
+
// Skip for virtual agents (team clones like _agent_yi) — each has its own worktree,
|
|
1286
|
+
// but checkpoint uses `git add -A` which could interfere with parallel work.
|
|
1287
|
+
const _isVirtualAgent = String(chatId).startsWith('_agent_') || String(chatId).startsWith('_scope_');
|
|
1288
|
+
if (!_isVirtualAgent) {
|
|
1289
|
+
(gitCheckpointAsync || gitCheckpoint)(session.cwd, prompt).catch?.(() => { });
|
|
1290
|
+
}
|
|
1291
|
+
log('INFO', `[TIMING:${chatId}] pre-spawn +${Date.now() - _t0}ms (engine:${runtime.name} started:${session.started})`);
|
|
1292
|
+
|
|
1293
|
+
// Use streaming mode to show progress
|
|
1294
|
+
// Telegram: edit status msg in-place; Feishu: edit or fallback to new messages
|
|
1295
|
+
let editFailed = false;
|
|
1296
|
+
let lastFallbackStatus = 0;
|
|
1297
|
+
const FALLBACK_THROTTLE = fallbackThrottleMs;
|
|
1298
|
+
const onStatus = async (status) => {
|
|
1299
|
+
try {
|
|
1300
|
+
if (typeof status !== 'string') return;
|
|
1301
|
+
|
|
1302
|
+
// __STREAM_TEXT__: streamed model text — edit card and track for final dedup
|
|
1303
|
+
if (status.startsWith('__STREAM_TEXT__')) {
|
|
1304
|
+
const content = status.slice('__STREAM_TEXT__'.length);
|
|
1305
|
+
// Set synchronously BEFORE await — this is the critical race fix.
|
|
1306
|
+
// flushStream(true) is called from the 'done' event (before process close),
|
|
1307
|
+
// so by setting here synchronously, _lastStatusCardContent is guaranteed to be
|
|
1308
|
+
// set before the child 'close' event fires and finalize() resolves.
|
|
1309
|
+
_lastStatusCardContent = content;
|
|
1310
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1311
|
+
const ok = await bot.editMessage(chatId, statusMsgId, content, _ackCardHeader);
|
|
1312
|
+
if (ok === false) editFailed = true;
|
|
1313
|
+
}
|
|
1314
|
+
return; // skip fallback — final reply logic will use existing card
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// __TOOL_OVERLAY__: text + tool status line — edit card but don't update _lastStatusCardContent
|
|
1318
|
+
if (status.startsWith('__TOOL_OVERLAY__')) {
|
|
1319
|
+
const content = status.slice('__TOOL_OVERLAY__'.length);
|
|
1320
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1321
|
+
await bot.editMessage(chatId, statusMsgId, content, _ackCardHeader);
|
|
1322
|
+
// intentionally NOT updating _lastStatusCardContent — overlay is transient
|
|
1323
|
+
}
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1217
1326
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1327
|
+
// Plain status (tool names before any text, milestone timers, etc.)
|
|
1328
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
1329
|
+
const ok = await bot.editMessage(chatId, statusMsgId, status, _ackCardHeader);
|
|
1330
|
+
if (ok !== false) {
|
|
1331
|
+
_lastStatusCardContent = status;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
editFailed = true;
|
|
1335
|
+
}
|
|
1336
|
+
// Fallback: send as new message with throttle to avoid spam
|
|
1337
|
+
const now = Date.now();
|
|
1338
|
+
if (now - lastFallbackStatus < FALLBACK_THROTTLE) return;
|
|
1339
|
+
lastFallbackStatus = now;
|
|
1340
|
+
await bot.sendMessage(chatId, status);
|
|
1341
|
+
} catch { /* ignore status update failures */ }
|
|
1230
1342
|
};
|
|
1231
|
-
await patchSessionSerialized(chatId, (cur) => {
|
|
1232
|
-
const engines = { ...(cur.engines || {}) };
|
|
1233
|
-
engines[runtime.name] = { ...(engines[runtime.name] || {}), id: safeNextId, started: true };
|
|
1234
|
-
return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
|
|
1235
|
-
});
|
|
1236
|
-
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
1237
|
-
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
1343
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
({
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
chatId,
|
|
1259
|
-
boundProjectKey || '',
|
|
1260
|
-
runtime,
|
|
1261
|
-
onSession,
|
|
1262
|
-
));
|
|
1263
|
-
|
|
1264
|
-
if (sessionId) await onSession(sessionId);
|
|
1265
|
-
|
|
1266
|
-
if (shouldRetryCodexResumeFallback({
|
|
1267
|
-
runtimeName: runtime.name,
|
|
1268
|
-
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1269
|
-
output,
|
|
1270
|
-
error,
|
|
1271
|
-
errorCode,
|
|
1272
|
-
canRetry: canRetryCodexResume(chatId),
|
|
1273
|
-
})) {
|
|
1274
|
-
markCodexResumeRetried(chatId);
|
|
1275
|
-
log('WARN', `Codex resume failed for ${chatId}, retrying once with fresh exec: ${String(error).slice(0, 120)}`);
|
|
1276
|
-
// Notify user explicitly — silent context loss is worse than a visible warning.
|
|
1277
|
-
await bot.sendMessage(chatId, '⚠️ Codex session 已过期,上下文丢失。正在以全新 session 重试,请在回复后补充必要背景。').catch(() => {});
|
|
1278
|
-
session = createSession(
|
|
1279
|
-
chatId,
|
|
1280
|
-
session.cwd,
|
|
1281
|
-
boundProject && boundProject.name ? boundProject.name : '',
|
|
1282
|
-
'codex'
|
|
1283
|
-
);
|
|
1284
|
-
const retryArgs = runtime.buildArgs({
|
|
1285
|
-
model,
|
|
1286
|
-
readOnly,
|
|
1287
|
-
daemonCfg,
|
|
1288
|
-
session,
|
|
1289
|
-
cwd: session.cwd,
|
|
1344
|
+
const wasCodexResumeAttempt = runtime.name === 'codex'
|
|
1345
|
+
&& !!(session && session.started && session.id && session.id !== '__continue__');
|
|
1346
|
+
const onSession = async (nextSessionId) => {
|
|
1347
|
+
const safeNextId = String(nextSessionId || '').trim();
|
|
1348
|
+
if (!safeNextId) return;
|
|
1349
|
+
const prevSessionId = session && session.id ? String(session.id) : '';
|
|
1350
|
+
const wasStarted = !!(session && session.started);
|
|
1351
|
+
session = {
|
|
1352
|
+
...session,
|
|
1353
|
+
id: safeNextId,
|
|
1354
|
+
engine: runtime.name,
|
|
1355
|
+
started: true,
|
|
1356
|
+
};
|
|
1357
|
+
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1358
|
+
const engines = { ...(cur.engines || {}) };
|
|
1359
|
+
engines[runtime.name] = { ...(engines[runtime.name] || {}), id: safeNextId, started: true };
|
|
1360
|
+
return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
|
|
1290
1361
|
});
|
|
1291
|
-
|
|
1292
|
-
|
|
1362
|
+
if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
|
|
1363
|
+
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
let output, error, errorCode, files, toolUsageLog, timedOut, usage, sessionId;
|
|
1368
|
+
try {
|
|
1293
1369
|
({
|
|
1294
1370
|
output,
|
|
1295
1371
|
error,
|
|
@@ -1300,8 +1376,8 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1300
1376
|
usage,
|
|
1301
1377
|
sessionId,
|
|
1302
1378
|
} = await spawnClaudeStreaming(
|
|
1303
|
-
|
|
1304
|
-
|
|
1379
|
+
args,
|
|
1380
|
+
fullPrompt,
|
|
1305
1381
|
session.cwd,
|
|
1306
1382
|
onStatus,
|
|
1307
1383
|
600000,
|
|
@@ -1310,240 +1386,331 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
1310
1386
|
runtime,
|
|
1311
1387
|
onSession,
|
|
1312
1388
|
));
|
|
1389
|
+
|
|
1313
1390
|
if (sessionId) await onSession(sessionId);
|
|
1391
|
+
|
|
1392
|
+
if (shouldRetryCodexResumeFallback({
|
|
1393
|
+
runtimeName: runtime.name,
|
|
1394
|
+
wasResumeAttempt: wasCodexResumeAttempt,
|
|
1395
|
+
output,
|
|
1396
|
+
error,
|
|
1397
|
+
errorCode,
|
|
1398
|
+
canRetry: canRetryCodexResume(chatId),
|
|
1399
|
+
})) {
|
|
1400
|
+
markCodexResumeRetried(chatId);
|
|
1401
|
+
log('WARN', `Codex resume failed for ${chatId}, retrying once with fresh exec: ${String(error).slice(0, 120)}`);
|
|
1402
|
+
// Notify user explicitly — silent context loss is worse than a visible warning.
|
|
1403
|
+
await bot.sendMessage(chatId, '⚠️ Codex session 已过期,上下文丢失。正在以全新 session 重试,请在回复后补充必要背景。').catch(() => { });
|
|
1404
|
+
session = createSession(
|
|
1405
|
+
sessionChatId,
|
|
1406
|
+
session.cwd,
|
|
1407
|
+
boundProject && boundProject.name ? boundProject.name : '',
|
|
1408
|
+
'codex'
|
|
1409
|
+
);
|
|
1410
|
+
const retryArgs = runtime.buildArgs({
|
|
1411
|
+
model,
|
|
1412
|
+
readOnly,
|
|
1413
|
+
daemonCfg,
|
|
1414
|
+
session,
|
|
1415
|
+
cwd: session.cwd,
|
|
1416
|
+
});
|
|
1417
|
+
// Prepend a context-loss marker so Codex knows this is a fresh session mid-conversation.
|
|
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}`;
|
|
1419
|
+
({
|
|
1420
|
+
output,
|
|
1421
|
+
error,
|
|
1422
|
+
errorCode,
|
|
1423
|
+
timedOut,
|
|
1424
|
+
files,
|
|
1425
|
+
toolUsageLog,
|
|
1426
|
+
usage,
|
|
1427
|
+
sessionId,
|
|
1428
|
+
} = await spawnClaudeStreaming(
|
|
1429
|
+
retryArgs,
|
|
1430
|
+
retryPrompt,
|
|
1431
|
+
session.cwd,
|
|
1432
|
+
onStatus,
|
|
1433
|
+
600000,
|
|
1434
|
+
chatId,
|
|
1435
|
+
boundProjectKey || '',
|
|
1436
|
+
runtime,
|
|
1437
|
+
onSession,
|
|
1438
|
+
));
|
|
1439
|
+
if (sessionId) await onSession(sessionId);
|
|
1440
|
+
}
|
|
1441
|
+
} catch (spawnErr) {
|
|
1442
|
+
clearInterval(typingTimer);
|
|
1443
|
+
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
1444
|
+
log('ERROR', `spawnClaudeStreaming crashed for ${chatId}: ${spawnErr.message}`);
|
|
1445
|
+
await bot.sendMessage(chatId, `❌ 内部错误: ${spawnErr.message}`).catch(() => { });
|
|
1446
|
+
return { ok: false, error: spawnErr.message };
|
|
1314
1447
|
}
|
|
1315
|
-
} catch (spawnErr) {
|
|
1316
1448
|
clearInterval(typingTimer);
|
|
1317
|
-
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => { });
|
|
1318
|
-
log('ERROR', `spawnClaudeStreaming crashed for ${chatId}: ${spawnErr.message}`);
|
|
1319
|
-
await bot.sendMessage(chatId, `❌ 内部错误: ${spawnErr.message}`).catch(() => { });
|
|
1320
|
-
return { ok: false, error: spawnErr.message };
|
|
1321
|
-
}
|
|
1322
|
-
clearInterval(typingTimer);
|
|
1323
1449
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
try {
|
|
1327
|
-
const signal = skillEvolution.extractSkillSignal(fullPrompt, output, error, files, session.cwd, toolUsageLog);
|
|
1328
|
-
if (signal) {
|
|
1329
|
-
skillEvolution.appendSkillSignal(signal);
|
|
1330
|
-
skillEvolution.checkHotEvolution(signal);
|
|
1331
|
-
}
|
|
1332
|
-
} catch (e) { log('WARN', `Skill evolution signal capture failed: ${e.message}`); }
|
|
1333
|
-
}
|
|
1450
|
+
// [PROTECTED] L0 lossless diary — see logRawSessionDiary() at file top
|
|
1451
|
+
logRawSessionDiary(fs, path, HOME, { chatId, prompt, output, error, projectKey: boundProjectKey });
|
|
1334
1452
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1453
|
+
// Skill evolution: capture signal + hot path heuristic check
|
|
1454
|
+
if (skillEvolution) {
|
|
1455
|
+
try {
|
|
1456
|
+
const signal = skillEvolution.extractSkillSignal(fullPrompt, output, error, files, session.cwd, toolUsageLog);
|
|
1457
|
+
if (signal) {
|
|
1458
|
+
skillEvolution.appendSkillSignal(signal);
|
|
1459
|
+
skillEvolution.checkHotEvolution(signal);
|
|
1460
|
+
}
|
|
1461
|
+
} catch (e) { log('WARN', `Skill evolution signal capture failed: ${e.message}`); }
|
|
1462
|
+
}
|
|
1339
1463
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1464
|
+
// statusMsgId is always available for final reply handling (edit or delete).
|
|
1465
|
+
const _statusMsgIdForReply = statusMsgId || null;
|
|
1466
|
+
|
|
1467
|
+
// Mentor post-flight debt registration (intense mode only).
|
|
1468
|
+
if (mentorEnabled && !mentorExcluded && !mentorSuppressed && mentorEngine && typeof mentorEngine.registerDebt === 'function' && output) {
|
|
1469
|
+
try {
|
|
1470
|
+
const mode = resolveMentorMode(mentorCfg);
|
|
1471
|
+
if (mode === 'intense') {
|
|
1472
|
+
const codeLines = countCodeLines(output);
|
|
1473
|
+
if (codeLines > 30) {
|
|
1474
|
+
const info = deriveProjectInfo(session && session.cwd ? session.cwd : '');
|
|
1475
|
+
const projectId = info && info.project_id ? info.project_id : 'proj_default';
|
|
1476
|
+
mentorEngine.registerDebt(projectId, String(prompt || '').slice(0, 120), codeLines);
|
|
1477
|
+
log('INFO', `[MENTOR] Registered reflection debt (${projectId}, lines=${codeLines})`);
|
|
1478
|
+
}
|
|
1351
1479
|
}
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
log('WARN', `Mentor post-flight failed: ${e.message}`);
|
|
1352
1482
|
}
|
|
1353
|
-
} catch (e) {
|
|
1354
|
-
log('WARN', `Mentor post-flight failed: ${e.message}`);
|
|
1355
1483
|
}
|
|
1356
|
-
}
|
|
1357
1484
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1485
|
+
// When Claude completes with no text output (pure tool work), send a done notice
|
|
1486
|
+
if (output === '' && !error) {
|
|
1487
|
+
// Special case: if dispatch_to was called, send a "forwarded" confirmation
|
|
1488
|
+
const dispatchedTargets = (toolUsageLog || [])
|
|
1489
|
+
.filter(t => t.tool === 'Bash' && typeof t.context === 'string' && t.context.includes('dispatch_to'))
|
|
1490
|
+
.map(t => { const m = t.context.match(/dispatch_to\s+(\S+)/); return m ? m[1] : null; })
|
|
1491
|
+
.filter(Boolean);
|
|
1492
|
+
if (dispatchedTargets.length > 0) {
|
|
1493
|
+
const allProjects = (config && config.projects) || {};
|
|
1494
|
+
const names = dispatchedTargets.map(k => (allProjects[k] && allProjects[k].name) || k).join('、');
|
|
1495
|
+
const doneMsg = await bot.sendMessage(chatId, `✉️ 已转达给 ${names},处理中…`);
|
|
1496
|
+
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1497
|
+
const wasNew = !session.started;
|
|
1498
|
+
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1499
|
+
return { ok: true };
|
|
1500
|
+
}
|
|
1501
|
+
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
1502
|
+
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
1503
|
+
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1370
1504
|
const wasNew = !session.started;
|
|
1371
|
-
if (wasNew) markSessionStarted(
|
|
1505
|
+
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1372
1506
|
return { ok: true };
|
|
1373
1507
|
}
|
|
1374
|
-
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
1375
|
-
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
1376
|
-
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
1377
|
-
const wasNew = !session.started;
|
|
1378
|
-
if (wasNew) markSessionStarted(chatId, engineName);
|
|
1379
|
-
return { ok: true };
|
|
1380
|
-
}
|
|
1381
1508
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1509
|
+
if (output) {
|
|
1510
|
+
if (runtime.name === 'codex') _codexResumeRetryTs.delete(String(chatId));
|
|
1511
|
+
// Detect provider/model errors disguised as output (e.g., "model not found", API errors)
|
|
1512
|
+
if (runtime.name === 'claude') {
|
|
1513
|
+
const activeProvCheck = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
1514
|
+
const builtinModelsCheck = ['sonnet', 'opus', 'haiku'];
|
|
1515
|
+
const looksLikeError = output.length < 300 && /\b(not found|invalid model|unauthorized|401|403|404|error|failed)\b/i.test(output);
|
|
1516
|
+
if (looksLikeError && (activeProvCheck !== 'anthropic' || !builtinModelsCheck.includes(model))) {
|
|
1517
|
+
try {
|
|
1518
|
+
config = fallbackToDefaultProvider(`output looks like error for ${activeProvCheck}/${model}`);
|
|
1519
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProvCheck}/${model} 疑似失败,已回退到 anthropic/opus\n输出: ${output.slice(0, 150)}`);
|
|
1520
|
+
} catch (fbErr) {
|
|
1521
|
+
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
1522
|
+
await bot.sendMarkdown(chatId, output);
|
|
1523
|
+
}
|
|
1524
|
+
return { ok: false, error: output };
|
|
1396
1525
|
}
|
|
1397
|
-
return { ok: false, error: output };
|
|
1398
1526
|
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Mark session as started after first successful call
|
|
1402
|
-
const wasNew = !session.started;
|
|
1403
|
-
if (wasNew) markSessionStarted(chatId, engineName);
|
|
1404
1527
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
cwd: session && session.cwd,
|
|
1409
|
-
homeDir: HOME,
|
|
1410
|
-
});
|
|
1411
|
-
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
1528
|
+
// Mark session as started after first successful call
|
|
1529
|
+
const wasNew = !session.started;
|
|
1530
|
+
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
1412
1531
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1532
|
+
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
1533
|
+
const chatCategory = classifyChatUsage(chatId, {
|
|
1534
|
+
projectKey: boundProjectKey || '',
|
|
1535
|
+
cwd: session && session.cwd,
|
|
1536
|
+
homeDir: HOME,
|
|
1537
|
+
});
|
|
1538
|
+
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
1415
1539
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
cleanOutput = `⚠️ **任务超时,以下是已完成的部分结果:**\n\n${cleanOutput}`;
|
|
1419
|
-
}
|
|
1540
|
+
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
1541
|
+
let { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
1420
1542
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const sessionCwd = path.resolve(normalizeCwd(session.cwd));
|
|
1425
|
-
for (const [, proj] of Object.entries(config.projects)) {
|
|
1426
|
-
if (!proj.cwd) continue;
|
|
1427
|
-
const projCwd = path.resolve(normalizeCwd(proj.cwd));
|
|
1428
|
-
if (sessionCwd === projCwd) { activeProject = proj; break; }
|
|
1543
|
+
// Timeout with partial results: prepend warning
|
|
1544
|
+
if (timedOut) {
|
|
1545
|
+
cleanOutput = `⚠️ **任务超时,以下是已完成的部分结果:**\n\n${cleanOutput}`;
|
|
1429
1546
|
}
|
|
1430
|
-
}
|
|
1431
1547
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
} catch (sendErr) {
|
|
1444
|
-
log('WARN', `sendCard/sendMarkdown failed (${sendErr.message}), falling back to sendMessage`);
|
|
1445
|
-
try { replyMsg = await bot.sendMessage(chatId, cleanOutput); } catch (e2) {
|
|
1446
|
-
log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
|
|
1548
|
+
// Match current session to a project for colored card display.
|
|
1549
|
+
// Prefer the bound project (known by virtual chatId or chat_agent_map) — avoids ambiguity
|
|
1550
|
+
// when multiple projects share the same cwd (e.g. team members with parent project cwd).
|
|
1551
|
+
let activeProject = boundProject || null;
|
|
1552
|
+
if (!activeProject && session && session.cwd && config && config.projects) {
|
|
1553
|
+
const sessionCwd = path.resolve(normalizeCwd(session.cwd));
|
|
1554
|
+
for (const [, proj] of Object.entries(config.projects)) {
|
|
1555
|
+
if (!proj.cwd) continue;
|
|
1556
|
+
const projCwd = path.resolve(normalizeCwd(proj.cwd));
|
|
1557
|
+
if (sessionCwd === projCwd) { activeProject = proj; break; }
|
|
1558
|
+
}
|
|
1447
1559
|
}
|
|
1448
|
-
}
|
|
1449
|
-
if (replyMsg && replyMsg.message_id && session) trackMsgSession(replyMsg.message_id, session);
|
|
1450
1560
|
|
|
1451
|
-
|
|
1561
|
+
let replyMsg;
|
|
1562
|
+
try {
|
|
1563
|
+
log('DEBUG', `[REPLY:${chatId}] statusMsgId=${statusMsgId} editFailed=${editFailed} activeProject=${activeProject && activeProject.name} lastCard=${_lastStatusCardContent ? _lastStatusCardContent.slice(0, 40) : 'null'}`);
|
|
1564
|
+
|
|
1565
|
+
// Strategy: always try to update the status card first (avoids sending a new card
|
|
1566
|
+
// while the old 🤔 card lingers, which would produce two messages).
|
|
1567
|
+
// If edit fails: try to delete the status card (awaited, not fire-and-forget).
|
|
1568
|
+
// If delete also fails: fall through to sending a new card.
|
|
1569
|
+
if (_statusMsgIdForReply && bot.editMessage) {
|
|
1570
|
+
// Skip redundant edit: streaming already wrote the final content to the card.
|
|
1571
|
+
// _lastStatusCardContent tracks the last __STREAM_TEXT__ write, so if it matches
|
|
1572
|
+
// cleanOutput the card is already showing the right content — no update needed.
|
|
1573
|
+
if (_lastStatusCardContent !== null && _lastStatusCardContent === cleanOutput) {
|
|
1574
|
+
log('DEBUG', `[REPLY:${chatId}] skipping editMessage — card already shows final content`);
|
|
1575
|
+
replyMsg = { message_id: _statusMsgIdForReply };
|
|
1576
|
+
} else {
|
|
1577
|
+
const editOk = await bot.editMessage(chatId, _statusMsgIdForReply, cleanOutput, _ackCardHeader);
|
|
1578
|
+
log('DEBUG', `[REPLY:${chatId}] editMessage result=${editOk}`);
|
|
1579
|
+
if (editOk !== false) {
|
|
1580
|
+
replyMsg = { message_id: _statusMsgIdForReply };
|
|
1581
|
+
} else if (bot.deleteMessage) {
|
|
1582
|
+
const deleted = await bot.deleteMessage(chatId, _statusMsgIdForReply).then(() => true).catch(() => false);
|
|
1583
|
+
log('DEBUG', `[REPLY:${chatId}] deleteMessage result=${deleted}`);
|
|
1584
|
+
if (!deleted) {
|
|
1585
|
+
// Both edit and delete failed — try one more edit attempt to avoid leaving 🤔
|
|
1586
|
+
log('WARN', `[REPLY:${chatId}] deleteMessage failed — status card may linger alongside new reply`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
} else if (_statusMsgIdForReply && bot.deleteMessage) {
|
|
1591
|
+
// No editMessage — delete the status card
|
|
1592
|
+
await bot.deleteMessage(chatId, _statusMsgIdForReply).catch(() => { });
|
|
1593
|
+
}
|
|
1452
1594
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1595
|
+
if (!replyMsg) {
|
|
1596
|
+
if (activeProject && bot.sendCard) {
|
|
1597
|
+
log('DEBUG', `[REPLY:${chatId}] sending sendCard`);
|
|
1598
|
+
replyMsg = await bot.sendCard(chatId, {
|
|
1599
|
+
title: `${activeProject.icon || '🤖'} ${activeProject.name || ''}`,
|
|
1600
|
+
body: cleanOutput,
|
|
1601
|
+
color: activeProject.color || 'blue',
|
|
1602
|
+
});
|
|
1603
|
+
log('DEBUG', `[REPLY:${chatId}] sendCard done msgId=${replyMsg && replyMsg.message_id}`);
|
|
1604
|
+
} else {
|
|
1605
|
+
log('DEBUG', `[REPLY:${chatId}] sending sendMarkdown`);
|
|
1606
|
+
replyMsg = await bot.sendMarkdown(chatId, cleanOutput);
|
|
1607
|
+
log('DEBUG', `[REPLY:${chatId}] sendMarkdown done msgId=${replyMsg && replyMsg.message_id}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
} catch (sendErr) {
|
|
1611
|
+
log('WARN', `sendCard/sendMarkdown failed (${sendErr.message}), falling back to sendMessage`);
|
|
1612
|
+
try { replyMsg = await bot.sendMessage(chatId, cleanOutput); } catch (e2) {
|
|
1613
|
+
log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (replyMsg && replyMsg.message_id && session) trackMsgSession(replyMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
|
|
1457
1617
|
|
|
1458
|
-
|
|
1459
|
-
if (runtime.name === 'claude' && wasNew && !getSessionName(session.id)) {
|
|
1460
|
-
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
1461
|
-
}
|
|
1618
|
+
await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
|
|
1462
1619
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
const memory = require('./memory');
|
|
1468
|
-
const pKey = boundProjectKey || '';
|
|
1469
|
-
const sessions = memory.recentSessions({ limit: 5, project: pKey });
|
|
1470
|
-
const factsRaw = memory.searchFacts('', { limit: 10, project: pKey });
|
|
1471
|
-
const facts = Array.isArray(factsRaw) ? factsRaw : [];
|
|
1472
|
-
memory.close();
|
|
1473
|
-
const snapshotContent = buildMemorySnapshotContent(sessions, facts);
|
|
1474
|
-
const agentId = boundProject.agent_id;
|
|
1475
|
-
if (refreshMemorySnapshot(agentId, snapshotContent, HOME)) {
|
|
1476
|
-
log('DEBUG', `[AGENT] Memory snapshot refreshed for ${agentId}`);
|
|
1477
|
-
}
|
|
1478
|
-
} catch { /* non-critical — memory module may not be available */ }
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
return { ok: !timedOut };
|
|
1482
|
-
} else {
|
|
1483
|
-
const errMsg = error || 'Unknown error';
|
|
1484
|
-
const userErrMsg = (errorCode === 'AUTH_REQUIRED' || errorCode === 'RATE_LIMIT')
|
|
1485
|
-
? errMsg
|
|
1486
|
-
: `Error: ${errMsg.slice(0, 200)}`;
|
|
1487
|
-
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
1488
|
-
|
|
1489
|
-
// If session not found (expired/deleted), create new and retry once (Claude path)
|
|
1490
|
-
if (runtime.name === 'claude' && (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use'))) {
|
|
1491
|
-
log('WARN', `Session ${session.id} unusable (${errMsg.includes('already in use') ? 'locked' : 'not found'}), creating new`);
|
|
1492
|
-
session = createSession(chatId, session.cwd, '', runtime.name);
|
|
1493
|
-
|
|
1494
|
-
const retryArgs = runtime.buildArgs({
|
|
1495
|
-
model,
|
|
1496
|
-
readOnly,
|
|
1497
|
-
daemonCfg,
|
|
1498
|
-
session,
|
|
1499
|
-
cwd: session.cwd,
|
|
1500
|
-
});
|
|
1620
|
+
// Timeout: also send the reason after the partial result
|
|
1621
|
+
if (timedOut && error) {
|
|
1622
|
+
try { await bot.sendMessage(chatId, error); } catch { /* */ }
|
|
1623
|
+
}
|
|
1501
1624
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
session.cwd,
|
|
1506
|
-
onStatus,
|
|
1507
|
-
600000,
|
|
1508
|
-
chatId,
|
|
1509
|
-
boundProjectKey || '',
|
|
1510
|
-
runtime,
|
|
1511
|
-
onSession,
|
|
1512
|
-
);
|
|
1513
|
-
if (retry.sessionId) await onSession(retry.sessionId);
|
|
1514
|
-
if (retry.output) {
|
|
1515
|
-
markSessionStarted(chatId, runtime.name);
|
|
1516
|
-
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
1517
|
-
await bot.sendMarkdown(chatId, retryClean);
|
|
1518
|
-
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
1519
|
-
return { ok: true };
|
|
1520
|
-
} else {
|
|
1521
|
-
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
1522
|
-
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1523
|
-
return { ok: false, error: retry.error || errMsg };
|
|
1625
|
+
// Auto-name: if this was the first message and session has no name, generate one
|
|
1626
|
+
if (runtime.name === 'claude' && wasNew && !getSessionName(session.id)) {
|
|
1627
|
+
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
1524
1628
|
}
|
|
1525
|
-
|
|
1526
|
-
// Auto-
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1529
|
-
const builtinModels = ENGINE_MODEL_CONFIG.claude.options;
|
|
1530
|
-
if (activeProv !== 'anthropic' || !builtinModels.includes(model)) {
|
|
1629
|
+
|
|
1630
|
+
// Auto-refresh memory-snapshot.md for this agent on first session message (fire-and-forget)
|
|
1631
|
+
if (wasNew && boundProject && boundProject.agent_id) {
|
|
1632
|
+
setImmediate(async () => {
|
|
1531
1633
|
try {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1634
|
+
const memory = require('./memory');
|
|
1635
|
+
const pKey = boundProjectKey || '';
|
|
1636
|
+
const sessions = memory.recentSessions({ limit: 5, project: pKey });
|
|
1637
|
+
const factsRaw = memory.searchFacts('', { limit: 10, project: pKey });
|
|
1638
|
+
const facts = Array.isArray(factsRaw) ? factsRaw : [];
|
|
1639
|
+
memory.close();
|
|
1640
|
+
const snapshotContent = buildMemorySnapshotContent(sessions, facts);
|
|
1641
|
+
const agentId = boundProject.agent_id;
|
|
1642
|
+
if (refreshMemorySnapshot(agentId, snapshotContent, HOME)) {
|
|
1643
|
+
log('DEBUG', `[AGENT] Memory snapshot refreshed for ${agentId}`);
|
|
1644
|
+
}
|
|
1645
|
+
} catch { /* non-critical — memory module may not be available */ }
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
return { ok: !timedOut };
|
|
1649
|
+
} else {
|
|
1650
|
+
const errMsg = error || 'Unknown error';
|
|
1651
|
+
const userErrMsg = (errorCode === 'AUTH_REQUIRED' || errorCode === 'RATE_LIMIT')
|
|
1652
|
+
? errMsg
|
|
1653
|
+
: `Error: ${errMsg.slice(0, 200)}`;
|
|
1654
|
+
log('ERROR', `ask${runtime.name === 'codex' ? 'Codex' : 'Claude'} failed for ${chatId}: ${errMsg.slice(0, 300)} (${errorCode || 'NO_CODE'})`);
|
|
1655
|
+
|
|
1656
|
+
// If session not found (expired/deleted), create new and retry once (Claude path)
|
|
1657
|
+
if (runtime.name === 'claude' && (errMsg.includes('not found') || errMsg.includes('No session') || errMsg.includes('already in use'))) {
|
|
1658
|
+
log('WARN', `Session ${session.id} unusable (${errMsg.includes('already in use') ? 'locked' : 'not found'}), creating new`);
|
|
1659
|
+
session = createSession(sessionChatId, session.cwd, '', runtime.name);
|
|
1660
|
+
|
|
1661
|
+
const retryArgs = runtime.buildArgs({
|
|
1662
|
+
model,
|
|
1663
|
+
readOnly,
|
|
1664
|
+
daemonCfg,
|
|
1665
|
+
session,
|
|
1666
|
+
cwd: session.cwd,
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
const retry = await spawnClaudeStreaming(
|
|
1670
|
+
retryArgs,
|
|
1671
|
+
fullPrompt,
|
|
1672
|
+
session.cwd,
|
|
1673
|
+
onStatus,
|
|
1674
|
+
600000,
|
|
1675
|
+
chatId,
|
|
1676
|
+
boundProjectKey || '',
|
|
1677
|
+
runtime,
|
|
1678
|
+
onSession,
|
|
1679
|
+
);
|
|
1680
|
+
if (retry.sessionId) await onSession(retry.sessionId);
|
|
1681
|
+
if (retry.output) {
|
|
1682
|
+
markSessionStarted(sessionChatId, runtime.name);
|
|
1683
|
+
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
1684
|
+
await bot.sendMarkdown(chatId, retryClean);
|
|
1685
|
+
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
1686
|
+
return { ok: true };
|
|
1687
|
+
} else {
|
|
1688
|
+
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
1689
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1690
|
+
return { ok: false, error: retry.error || errMsg };
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus (Claude path only)
|
|
1694
|
+
if (runtime.name === 'claude') {
|
|
1695
|
+
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
1696
|
+
const builtinModels = ENGINE_MODEL_CONFIG.claude.options;
|
|
1697
|
+
if ((activeProv !== 'anthropic' || !builtinModels.includes(model)) && !errMsg.includes('Stopped by user')) {
|
|
1698
|
+
try {
|
|
1699
|
+
config = fallbackToDefaultProvider(`${activeProv}/${model} error: ${errMsg.slice(0, 100)}`);
|
|
1700
|
+
await bot.sendMessage(chatId, `⚠️ ${activeProv}/${model} 失败,已回退到 anthropic/opus\n原因: ${errMsg.slice(0, 100)}`);
|
|
1701
|
+
} catch (fallbackErr) {
|
|
1702
|
+
log('ERROR', `Fallback failed: ${fallbackErr.message}`);
|
|
1703
|
+
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1704
|
+
}
|
|
1705
|
+
} else {
|
|
1536
1706
|
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1537
1707
|
}
|
|
1538
1708
|
} else {
|
|
1539
1709
|
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1540
1710
|
}
|
|
1541
|
-
|
|
1542
|
-
try { await bot.sendMessage(chatId, userErrMsg); } catch { /* */ }
|
|
1711
|
+
return { ok: false, error: errMsg, errorCode };
|
|
1543
1712
|
}
|
|
1544
|
-
return { ok: false, error: errMsg, errorCode };
|
|
1545
1713
|
}
|
|
1546
|
-
}
|
|
1547
1714
|
|
|
1548
1715
|
} catch (fatalErr) { // ── safety-net-catch ──
|
|
1549
1716
|
clearInterval(typingTimer);
|