metame-cli 1.5.25 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/agent-layer.js +36 -0
- package/scripts/core/wiki-db.js +404 -0
- package/scripts/core/wiki-prompt.js +88 -0
- package/scripts/core/wiki-slug.js +66 -0
- package/scripts/core/wiki-staleness.js +18 -0
- package/scripts/daemon-agent-commands.js +10 -4
- package/scripts/daemon-bridges.js +64 -3
- package/scripts/daemon-claude-engine.js +62 -8
- package/scripts/daemon-command-router.js +15 -0
- package/scripts/daemon-default.yaml +2 -3
- package/scripts/daemon-ops-commands.js +9 -18
- package/scripts/daemon-session-commands.js +4 -0
- package/scripts/daemon-warm-pool.js +15 -0
- package/scripts/daemon-wiki.js +298 -0
- package/scripts/daemon.js +6 -3
- package/scripts/distill.js +1 -1
- package/scripts/docs/file-transfer.md +0 -1
- package/scripts/docs/maintenance-manual.md +2 -55
- package/scripts/docs/pointer-map.md +0 -34
- package/scripts/hooks/intent-file-transfer.js +1 -2
- package/scripts/memory-search.js +17 -2
- package/scripts/memory-wiki-schema.js +96 -0
- package/scripts/memory.js +88 -3
- package/scripts/signal-capture.js +1 -1
- package/scripts/skill-evolution.js +2 -11
- package/scripts/wiki-reflect-build.js +117 -0
- package/scripts/wiki-reflect-export.js +333 -0
- package/scripts/wiki-reflect-query.js +109 -0
- package/scripts/wiki-reflect.js +305 -0
|
@@ -24,6 +24,7 @@ function createBridgeStarter(deps) {
|
|
|
24
24
|
saveState,
|
|
25
25
|
getSession,
|
|
26
26
|
restoreSessionFromReply,
|
|
27
|
+
releaseWarmPool,
|
|
27
28
|
handleCommand,
|
|
28
29
|
pipeline, // message pipeline for per-chatId serial execution
|
|
29
30
|
pendingActivations, // optional — used to show smart activation hint
|
|
@@ -224,6 +225,21 @@ function createBridgeStarter(deps) {
|
|
|
224
225
|
},
|
|
225
226
|
});
|
|
226
227
|
}
|
|
228
|
+
|
|
229
|
+
function _createPipelineTarget({ pipelineChatId, effectiveChatId, bot }) {
|
|
230
|
+
const replyChatId = String(pipelineChatId || '').trim();
|
|
231
|
+
const processChatId = String(effectiveChatId || pipelineChatId || '').trim();
|
|
232
|
+
if (!replyChatId || !processChatId) {
|
|
233
|
+
return { processChatId: replyChatId || processChatId, bot };
|
|
234
|
+
}
|
|
235
|
+
if (replyChatId === processChatId) {
|
|
236
|
+
return { processChatId, bot };
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
processChatId,
|
|
240
|
+
bot: _createTeamProxyBot(bot, replyChatId),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
227
243
|
// Get team member's working directory inside the source tree, never under ~/.metame.
|
|
228
244
|
// Creates agents/<key>/ directory by default, or ensures an explicit member.cwd exists.
|
|
229
245
|
function _getMemberCwd(parentCwd, key, explicitCwd = null) {
|
|
@@ -862,6 +878,11 @@ function createBridgeStarter(deps) {
|
|
|
862
878
|
}
|
|
863
879
|
if (mapped.id) {
|
|
864
880
|
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
881
|
+
// Evict warm process so next spawn uses --resume with the restored session
|
|
882
|
+
if (typeof releaseWarmPool === 'function') {
|
|
883
|
+
const _logicalKey = String(mapped.logicalChatId || '').trim();
|
|
884
|
+
if (_logicalKey) releaseWarmPool(_logicalKey);
|
|
885
|
+
}
|
|
865
886
|
}
|
|
866
887
|
_replyAgentKey = mapped.agentKey || null;
|
|
867
888
|
} else {
|
|
@@ -871,12 +892,29 @@ function createBridgeStarter(deps) {
|
|
|
871
892
|
_replyMappingFound = true;
|
|
872
893
|
_replyAgentKey = _parentMapping.agentKey || null;
|
|
873
894
|
log('INFO', `Feishu topic inherited root mapping agentKey=${_replyAgentKey || 'main'} parentId=${parentId}`);
|
|
895
|
+
// Restore session from topic root (same as quoted reply) so 指定回复 resumes context
|
|
896
|
+
if (_parentMapping.id && typeof restoreSessionFromReply === 'function') {
|
|
897
|
+
restoreSessionFromReply(chatId, _parentMapping);
|
|
898
|
+
log('INFO', `Session restored via topic root: ${_parentMapping.id.slice(0, 8)} (${path.basename(_parentMapping.cwd || '~')})`);
|
|
899
|
+
// Evict warm process so next spawn uses --resume with the restored session
|
|
900
|
+
if (typeof releaseWarmPool === 'function') {
|
|
901
|
+
const _logicalKey = String(_parentMapping.logicalChatId || '').trim();
|
|
902
|
+
if (_logicalKey) releaseWarmPool(_logicalKey);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
874
905
|
}
|
|
875
906
|
|
|
876
907
|
// Helper: set/clear sticky on shared state object and persist
|
|
877
908
|
// Use pipelineChatId so each topic gets independent sticky state
|
|
878
909
|
const _chatKey = String(pipelineChatId);
|
|
879
910
|
const _rawChatKey = _threadRawChatId(_chatKey);
|
|
911
|
+
const _topicMainRoute = !!(
|
|
912
|
+
threadRootId
|
|
913
|
+
&& _parentMapping
|
|
914
|
+
&& !_isQuotedReply
|
|
915
|
+
&& _parentMapping.logicalChatId
|
|
916
|
+
&& !String(_parentMapping.logicalChatId).startsWith('_agent_')
|
|
917
|
+
);
|
|
880
918
|
const _setSticky = (key) => {
|
|
881
919
|
if (!_st.team_sticky) _st.team_sticky = {};
|
|
882
920
|
_st.team_sticky[_chatKey] = key;
|
|
@@ -899,6 +937,11 @@ function createBridgeStarter(deps) {
|
|
|
899
937
|
saveState(_st);
|
|
900
938
|
};
|
|
901
939
|
let _stickyKey = (_st.team_sticky || {})[_chatKey] || (_st.team_sticky || {})[_rawChatKey] || null;
|
|
940
|
+
const _pipelineTarget = _createPipelineTarget({
|
|
941
|
+
pipelineChatId,
|
|
942
|
+
effectiveChatId: _topicMainRoute ? chatId : pipelineChatId,
|
|
943
|
+
bot,
|
|
944
|
+
});
|
|
902
945
|
|
|
903
946
|
// Team group routing: if bound project has a team array, check message for member nickname
|
|
904
947
|
// Non-/stop slash commands bypass team routing → handled by main project
|
|
@@ -978,7 +1021,13 @@ function createBridgeStarter(deps) {
|
|
|
978
1021
|
// Cases b & c: no agentKey (main agent) or stale/unknown agentKey
|
|
979
1022
|
_clearSticky();
|
|
980
1023
|
log('INFO', `Quoted reply → route to main (agentKey=${_replyAgentKey} mappingFound=${_replyMappingFound})`);
|
|
981
|
-
await pipeline.processMessage(
|
|
1024
|
+
await pipeline.processMessage(_pipelineTarget.processChatId, trimmedText, {
|
|
1025
|
+
bot: _pipelineTarget.bot,
|
|
1026
|
+
config: liveCfg,
|
|
1027
|
+
executeTaskByName,
|
|
1028
|
+
senderId: acl.senderId,
|
|
1029
|
+
readOnly: acl.readOnly,
|
|
1030
|
+
});
|
|
982
1031
|
return;
|
|
983
1032
|
}
|
|
984
1033
|
// 1. Explicit nickname → route + set sticky
|
|
@@ -1034,7 +1083,13 @@ function createBridgeStarter(deps) {
|
|
|
1034
1083
|
return;
|
|
1035
1084
|
}
|
|
1036
1085
|
try {
|
|
1037
|
-
await pipeline.processMessage(
|
|
1086
|
+
await pipeline.processMessage(_pipelineTarget.processChatId, rest, {
|
|
1087
|
+
bot: _pipelineTarget.bot,
|
|
1088
|
+
config: liveCfg,
|
|
1089
|
+
executeTaskByName,
|
|
1090
|
+
senderId: acl.senderId,
|
|
1091
|
+
readOnly: acl.readOnly,
|
|
1092
|
+
});
|
|
1038
1093
|
} catch (e) {
|
|
1039
1094
|
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
1040
1095
|
bot.sendMessage(pipelineChatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
@@ -1058,7 +1113,13 @@ function createBridgeStarter(deps) {
|
|
|
1058
1113
|
}
|
|
1059
1114
|
|
|
1060
1115
|
try {
|
|
1061
|
-
await pipeline.processMessage(
|
|
1116
|
+
await pipeline.processMessage(_pipelineTarget.processChatId, text, {
|
|
1117
|
+
bot: _pipelineTarget.bot,
|
|
1118
|
+
config: liveCfg,
|
|
1119
|
+
executeTaskByName,
|
|
1120
|
+
senderId: acl.senderId,
|
|
1121
|
+
readOnly: acl.readOnly,
|
|
1122
|
+
});
|
|
1062
1123
|
} catch (e) {
|
|
1063
1124
|
log('ERROR', `Feishu handleCommand failed for ${chatId}: ${e.message}`);
|
|
1064
1125
|
bot.sendMessage(pipelineChatId, `❌ 命令执行失败: ${e.message}`).catch(() => {});
|
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
_private: { resolveCodexPermissionProfile },
|
|
11
11
|
} = require('./daemon-engine-runtime');
|
|
12
12
|
const { rawChatId } = require('./core/thread-chat-id');
|
|
13
|
-
const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
|
|
13
|
+
const { buildAgentContextForEngine, buildMemorySnapshotContent, selectSnapshotContext, refreshMemorySnapshot } = require('./agent-layer');
|
|
14
14
|
const {
|
|
15
15
|
adaptDaemonHintForEngine,
|
|
16
16
|
buildAgentHint,
|
|
@@ -84,6 +84,7 @@ function createClaudeEngine(deps) {
|
|
|
84
84
|
isContentFile,
|
|
85
85
|
sendFileButtons,
|
|
86
86
|
findSessionFile,
|
|
87
|
+
listRecentSessions,
|
|
87
88
|
getSession,
|
|
88
89
|
getSessionForEngine,
|
|
89
90
|
createSession,
|
|
@@ -102,6 +103,7 @@ function createClaudeEngine(deps) {
|
|
|
102
103
|
touchInteraction,
|
|
103
104
|
statusThrottleMs = 3000,
|
|
104
105
|
fallbackThrottleMs = 8000,
|
|
106
|
+
autoSyncMinGapMs = 60_000,
|
|
105
107
|
getEngineRuntime: injectedGetEngineRuntime,
|
|
106
108
|
getDefaultEngine: _getDefaultEngine,
|
|
107
109
|
warmPool,
|
|
@@ -1362,6 +1364,36 @@ function createClaudeEngine(deps) {
|
|
|
1362
1364
|
await patchSessionSerialized(sessionChatId, (cur) => ({ ...cur, cwd: effectiveCwd }));
|
|
1363
1365
|
}
|
|
1364
1366
|
|
|
1367
|
+
// Auto-sync: when daemon is idle (no warm process), check if a newer Claude session exists
|
|
1368
|
+
// in the project directory (e.g., created from Claude Code CLI on the same computer).
|
|
1369
|
+
// This keeps phone (Feishu/mobile) and computer (Claude Code CLI) sessions in sync.
|
|
1370
|
+
const _hasWarm = !!(warmPool && typeof warmPool.hasWarm === 'function' && warmPool.hasWarm(sessionChatId));
|
|
1371
|
+
if (runtime.name === 'claude' && !String(sessionChatId).startsWith('_agent_') &&
|
|
1372
|
+
!_hasWarm && session.started && session.id && session.id !== '__continue__' && session.cwd) {
|
|
1373
|
+
const _recentInProject = typeof listRecentSessions === 'function'
|
|
1374
|
+
? listRecentSessions(2, session.cwd, 'claude') : [];
|
|
1375
|
+
const _currentMtime = (() => {
|
|
1376
|
+
try {
|
|
1377
|
+
const _f = typeof findSessionFile === 'function' ? findSessionFile(session.id) : null;
|
|
1378
|
+
return _f ? fs.statSync(_f).mtimeMs : 0;
|
|
1379
|
+
} catch { return 0; }
|
|
1380
|
+
})();
|
|
1381
|
+
const _newerSession = _recentInProject.find(
|
|
1382
|
+
s => s.sessionId !== session.id && (s.fileMtime || 0) - _currentMtime > autoSyncMinGapMs
|
|
1383
|
+
);
|
|
1384
|
+
if (_newerSession) {
|
|
1385
|
+
const _gapSec = Math.round(((_newerSession.fileMtime || 0) - _currentMtime) / 1000);
|
|
1386
|
+
log('INFO', `[AutoSync] ${String(sessionChatId).slice(-8)}: switching ${session.id.slice(0, 8)} -> ${_newerSession.sessionId.slice(0, 8)} (newer by ${_gapSec}s)`);
|
|
1387
|
+
await patchSessionSerialized(sessionChatId, (cur) => {
|
|
1388
|
+
const _engines = { ...(cur.engines || {}) };
|
|
1389
|
+
_engines.claude = { ...(_engines.claude || {}), id: _newerSession.sessionId, started: true };
|
|
1390
|
+
return { ...cur, engines: _engines };
|
|
1391
|
+
});
|
|
1392
|
+
session = { ...session, id: _newerSession.sessionId };
|
|
1393
|
+
bot.sendMessage(chatId, `🔄 已自动同步到最新 session:\`${_newerSession.sessionId.slice(0, 8)}\``).catch(() => { });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1365
1397
|
// Warm pool: check if a persistent process is available for this session (Claude only).
|
|
1366
1398
|
// Declared early so downstream logic can skip expensive operations when reusing warm process.
|
|
1367
1399
|
const _warmSessionKey = sessionChatId;
|
|
@@ -2148,14 +2180,30 @@ function createClaudeEngine(deps) {
|
|
|
2148
2180
|
if (dispatchedTargets.length > 0) {
|
|
2149
2181
|
const allProjects = (config && config.projects) || {};
|
|
2150
2182
|
const names = dispatchedTargets.map(k => (allProjects[k] && allProjects[k].name) || k).join('、');
|
|
2151
|
-
const
|
|
2183
|
+
const fwdText = `✉️ 已转达给 ${names},处理中…`;
|
|
2184
|
+
let doneMsg;
|
|
2185
|
+
if (statusMsgId && bot.editMessage) {
|
|
2186
|
+
await bot.editMessage(chatId, statusMsgId, fwdText, _ackCardHeader).catch(() => {});
|
|
2187
|
+
doneMsg = { message_id: statusMsgId };
|
|
2188
|
+
} else {
|
|
2189
|
+
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => {});
|
|
2190
|
+
doneMsg = await bot.sendMessage(chatId, fwdText);
|
|
2191
|
+
}
|
|
2152
2192
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, projectKeyFromVirtualChatId(chatId));
|
|
2153
2193
|
const wasNew = !session.started;
|
|
2154
2194
|
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
2155
2195
|
return { ok: true };
|
|
2156
2196
|
}
|
|
2157
2197
|
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
2158
|
-
const
|
|
2198
|
+
const doneText = `✅ 完成${filesDesc}`;
|
|
2199
|
+
let doneMsg;
|
|
2200
|
+
if (statusMsgId && bot.editMessage) {
|
|
2201
|
+
await bot.editMessage(chatId, statusMsgId, doneText, _ackCardHeader).catch(() => {});
|
|
2202
|
+
doneMsg = { message_id: statusMsgId };
|
|
2203
|
+
} else {
|
|
2204
|
+
if (statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, statusMsgId).catch(() => {});
|
|
2205
|
+
doneMsg = await bot.sendMessage(chatId, doneText);
|
|
2206
|
+
}
|
|
2159
2207
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, projectKeyFromVirtualChatId(chatId));
|
|
2160
2208
|
const wasNew = !session.started;
|
|
2161
2209
|
if (wasNew) markSessionStarted(sessionChatId, engineName);
|
|
@@ -2338,12 +2386,18 @@ function createClaudeEngine(deps) {
|
|
|
2338
2386
|
setImmediate(async () => {
|
|
2339
2387
|
try {
|
|
2340
2388
|
const memory = require('./memory');
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2389
|
+
const snapshotCtx = selectSnapshotContext(memory, {
|
|
2390
|
+
projectHints: [
|
|
2391
|
+
boundProjectKey,
|
|
2392
|
+
boundProject.project_key,
|
|
2393
|
+
boundProject.name,
|
|
2394
|
+
boundProject.agent_id,
|
|
2395
|
+
],
|
|
2396
|
+
sessionLimit: 5,
|
|
2397
|
+
factLimit: 10,
|
|
2398
|
+
});
|
|
2345
2399
|
memory.close();
|
|
2346
|
-
const snapshotContent = buildMemorySnapshotContent(sessions, facts);
|
|
2400
|
+
const snapshotContent = buildMemorySnapshotContent(snapshotCtx.sessions, snapshotCtx.facts);
|
|
2347
2401
|
const agentId = boundProject.agent_id;
|
|
2348
2402
|
if (refreshMemorySnapshot(agentId, snapshotContent, HOME)) {
|
|
2349
2403
|
log('DEBUG', `[AGENT] Memory snapshot refreshed for ${agentId}`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
4
4
|
const { createAgentIntentHandler } = require('./daemon-agent-intent');
|
|
5
5
|
const { rawChatId: extractOriginalChatId, isThreadChatId } = require('./core/thread-chat-id');
|
|
6
|
+
const { createWikiCommandHandler } = require('./daemon-wiki');
|
|
6
7
|
|
|
7
8
|
function createCommandRouter(deps) {
|
|
8
9
|
const {
|
|
@@ -30,6 +31,7 @@ function createCommandRouter(deps) {
|
|
|
30
31
|
pendingActivations,
|
|
31
32
|
agentFlowTtlMs,
|
|
32
33
|
getDefaultEngine,
|
|
34
|
+
getDb, // optional — () → DatabaseSync (for wiki commands)
|
|
33
35
|
} = deps;
|
|
34
36
|
|
|
35
37
|
|
|
@@ -462,6 +464,19 @@ function createCommandRouter(deps) {
|
|
|
462
464
|
return;
|
|
463
465
|
}
|
|
464
466
|
|
|
467
|
+
// /wiki — knowledge wiki commands
|
|
468
|
+
if (text.startsWith('/wiki') && getDb) {
|
|
469
|
+
const wikiProviders = providerMod ? {
|
|
470
|
+
callHaiku: (...args) => providerMod.callHaiku(...args),
|
|
471
|
+
buildDistillEnv: (...args) => providerMod.buildDistillEnv(...args),
|
|
472
|
+
} : null;
|
|
473
|
+
const wikiOutputDir = config && config.daemon && config.daemon.wiki_output_dir
|
|
474
|
+
? config.daemon.wiki_output_dir.replace(/^~/, process.env.HOME || '')
|
|
475
|
+
: null;
|
|
476
|
+
const { handleWikiCommand } = createWikiCommandHandler({ getDb, providers: wikiProviders, wikiOutputDir, log });
|
|
477
|
+
if (await handleWikiCommand({ bot, chatId, text })) return;
|
|
478
|
+
}
|
|
479
|
+
|
|
465
480
|
// /btw — quick side question (read-only, concise, bypasses cooldown)
|
|
466
481
|
if (/^\/btw(\s|$)/i.test(text)) {
|
|
467
482
|
const btwQuestion = text.replace(/^\/btw\s*/i, '').trim();
|
|
@@ -55,7 +55,6 @@ heartbeat:
|
|
|
55
55
|
type: script
|
|
56
56
|
command: node ~/.metame/distill.js
|
|
57
57
|
interval: 4h
|
|
58
|
-
timeout: 5m
|
|
59
58
|
precondition: "test -s ~/.metame/raw_signals.jsonl"
|
|
60
59
|
require_idle: true
|
|
61
60
|
notify: false
|
|
@@ -85,7 +84,6 @@ heartbeat:
|
|
|
85
84
|
type: script
|
|
86
85
|
command: node ~/.metame/skill-evolution.js
|
|
87
86
|
interval: 12h
|
|
88
|
-
timeout: 5m
|
|
89
87
|
precondition: "test -s ~/.metame/skill_signals.jsonl"
|
|
90
88
|
require_idle: true
|
|
91
89
|
notify: false
|
|
@@ -116,7 +114,7 @@ heartbeat:
|
|
|
116
114
|
at: "01:30"
|
|
117
115
|
require_idle: true
|
|
118
116
|
notify: false
|
|
119
|
-
enabled:
|
|
117
|
+
enabled: true
|
|
120
118
|
|
|
121
119
|
# Legacy flat tasks (no project isolation). New tasks should go under projects: above.
|
|
122
120
|
# Examples — uncomment or add your own:
|
|
@@ -166,6 +164,7 @@ budget:
|
|
|
166
164
|
daemon:
|
|
167
165
|
model: sonnet # sonnet, opus, haiku — model for mobile sessions
|
|
168
166
|
log_max_size: 1048576
|
|
167
|
+
wiki_output_dir: ~/Documents/ObsidianVault/MetaMe/wiki # Obsidian vault wiki subdirectory
|
|
169
168
|
heartbeat_check_interval: 60
|
|
170
169
|
# Skip all permission prompts for mobile sessions (full access)
|
|
171
170
|
# Mobile users can't click "allow" — so we pre-authorize everything.
|
|
@@ -87,30 +87,22 @@ function createOpsCommandHandler(deps) {
|
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
89
|
let diffFiles = '';
|
|
90
|
-
let diffFailed = false;
|
|
91
90
|
const _wh = process.platform === 'win32' ? { windowsHide: true } : {};
|
|
92
|
-
try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000, ..._wh }).trim(); } catch {
|
|
91
|
+
try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000, ..._wh }).trim(); } catch { }
|
|
93
92
|
const changedFiles = diffFiles ? diffFiles.split('\n').filter(Boolean) : [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
// Restore only changed files (not entire worktree) to preserve user's manual edits
|
|
102
|
-
if (changedFiles.length > 0) {
|
|
103
|
-
execFileSync('git', ['checkout', match.hash, '--', ...changedFiles], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
104
|
-
} else {
|
|
105
|
-
// diff failed but we still reset — fallback to full restore
|
|
106
|
-
execFileSync('git', ['checkout', match.hash, '--', '.'], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
107
|
-
}
|
|
93
|
+
// Reset HEAD to checkpoint's parent (removes any commits Claude made)
|
|
94
|
+
if (match.parentHash) {
|
|
95
|
+
execSync(`git reset --hard ${match.parentHash}`, { cwd, stdio: 'ignore', timeout: 10000, ..._wh });
|
|
96
|
+
}
|
|
97
|
+
// Restore only files touched between the checkpoint and HEAD; unrelated user edits must survive.
|
|
98
|
+
if (changedFiles.length > 0) {
|
|
99
|
+
execFileSync('git', ['checkout', match.hash, '--', ...changedFiles], { cwd, stdio: 'ignore', timeout: 10000 });
|
|
108
100
|
}
|
|
109
101
|
// Truncate context to checkpoint time (covers multi-turn rollback)
|
|
110
102
|
truncateSessionToCheckpoint(session.id, match.message);
|
|
103
|
+
const fileList = changedFiles.map(f => path.basename(f)).join(', ');
|
|
111
104
|
const fileCount = changedFiles.length;
|
|
112
105
|
let msg = `⏪ 已回退到 ${cpDisplayLabel(match.message)}`;
|
|
113
|
-
const fileList = changedFiles.map(f => path.basename(f)).join(', ');
|
|
114
106
|
if (fileCount > 0) msg += `\n📁 ${fileCount} 个文件恢复: ${fileList}`;
|
|
115
107
|
log('INFO', `/undo <hash> executed for ${chatId}: reset to ${match.hash.slice(0, 8)}, files=${fileCount}`);
|
|
116
108
|
await bot.sendMessage(chatId, msg);
|
|
@@ -251,7 +243,6 @@ function createOpsCommandHandler(deps) {
|
|
|
251
243
|
if (cpMatch.parentHash) {
|
|
252
244
|
execSync(`git reset --hard ${cpMatch.parentHash}`, { cwd: cwd2, stdio: 'ignore', timeout: 10000, ..._wh2 });
|
|
253
245
|
}
|
|
254
|
-
// Restore only changed files (not entire worktree) to preserve user's manual edits
|
|
255
246
|
execFileSync('git', ['checkout', cpMatch.hash, '--', ...changedFiles2], { cwd: cwd2, stdio: 'ignore', timeout: 10000 });
|
|
256
247
|
gitMsg2 = `\n📁 ${changedFiles2.length} 个文件已恢复`;
|
|
257
248
|
cleanupCheckpoints(cwd2);
|
|
@@ -36,6 +36,7 @@ function createSessionCommandHandler(deps) {
|
|
|
36
36
|
getSessionRecentContext,
|
|
37
37
|
getSessionRecentDialogue,
|
|
38
38
|
getDefaultEngine = () => 'claude',
|
|
39
|
+
releaseWarmPool,
|
|
39
40
|
} = deps;
|
|
40
41
|
|
|
41
42
|
function normalizeEngineName(name) {
|
|
@@ -412,10 +413,13 @@ function createSessionCommandHandler(deps) {
|
|
|
412
413
|
return true;
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
const sessionKeyForWarm = getSessionRoute(chatId).sessionChatId;
|
|
415
417
|
const state2 = loadState();
|
|
416
418
|
const targetEngine = normalizeEngineName(target.engine) || getCurrentEngine(chatId);
|
|
417
419
|
const attached = attachResolvedTarget(state2, chatId, targetEngine, target, target.projectPath || HOME);
|
|
418
420
|
saveState(state2);
|
|
421
|
+
// Evict warm process so next spawn uses --resume <new-session-id>
|
|
422
|
+
if (typeof releaseWarmPool === 'function') releaseWarmPool(sessionKeyForWarm);
|
|
419
423
|
|
|
420
424
|
const recentCtx = typeof getSessionRecentContext === 'function'
|
|
421
425
|
? getSessionRecentContext(target.sessionId)
|
|
@@ -161,6 +161,20 @@ function createWarmPool(deps) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Non-destructive validity check. Returns true if a live warm process exists for the key.
|
|
166
|
+
* Mirrors acquireWarm's dead-process checks without consuming the entry.
|
|
167
|
+
*/
|
|
168
|
+
function hasWarm(sessionKey) {
|
|
169
|
+
const entry = pool.get(sessionKey);
|
|
170
|
+
if (!entry) return false;
|
|
171
|
+
if (entry.child.killed || entry.child.exitCode !== null) {
|
|
172
|
+
_cleanup(sessionKey);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
/**
|
|
165
179
|
* Build the stream-json user message for stdin.
|
|
166
180
|
*/
|
|
@@ -179,6 +193,7 @@ function createWarmPool(deps) {
|
|
|
179
193
|
releaseWarm,
|
|
180
194
|
releaseAll,
|
|
181
195
|
buildStreamMessage,
|
|
196
|
+
hasWarm,
|
|
182
197
|
_pool: pool,
|
|
183
198
|
};
|
|
184
199
|
}
|