mocode-ai 0.6.10 → 0.7.1
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 +4 -3
- package/README.zh-CN.md +3 -3
- package/dist/agent/core.js +162 -108
- package/dist/agent/index.js +20 -0
- package/dist/agent/spawn.js +9 -1
- package/dist/config/index.js +27 -5
- package/dist/i18n/index.js +34 -0
- package/dist/index.js +1 -1
- package/dist/llm/index.js +6 -2
- package/dist/mcp/index.js +15 -2
- package/dist/permissions/index.js +110 -93
- package/dist/repl/index.js +111 -21
- package/dist/rollback/index.js +31 -0
- package/dist/session/index.js +1 -0
- package/dist/session/persist.js +13 -5
- package/dist/session/trace.js +26 -0
- package/dist/tools/builtins/index.js +35 -4
- package/dist/tools/builtins/run-command.js +115 -66
- package/dist/tools/builtins/task.js +5 -2
- package/dist/tools/constants.js +5 -1
- package/dist/tools/registry.js +108 -25
- package/dist/ui/prompt.js +64 -11
- package/dist/verification/affected.js +149 -0
- package/dist/verification/discovery.js +40 -0
- package/dist/verification/index.js +163 -0
- package/dist/verification/profile.js +237 -0
- package/dist/verification/types.js +1 -0
- package/package.json +5 -2
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
7
7
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
@@ -16,9 +16,9 @@ import * as mouse from '../ui/mouse.js';
|
|
|
16
16
|
import * as batch from '../ui/batch.js';
|
|
17
17
|
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
18
18
|
import { promptIntervention } from '../ui/intervention.js';
|
|
19
|
-
import {
|
|
19
|
+
import { registerToolsExtension } from '../tools/registry.js';
|
|
20
20
|
import { initializeAllMcp, getMcpTools, closeAllMcp } from '../mcp/index.js';
|
|
21
|
-
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, } from '../llm/index.js';
|
|
21
|
+
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, chatTools, } from '../llm/index.js';
|
|
22
22
|
import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT, } from '../attachments/image.js';
|
|
23
23
|
import { modelSupportsVision } from '../llm/capabilities.js';
|
|
24
24
|
import { computePruneStats } from '../context/relevance.js';
|
|
@@ -63,6 +63,13 @@ function buildSlashCommands() {
|
|
|
63
63
|
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
64
64
|
],
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: '/subagent', desc: d('commands.subagent'), children: [
|
|
68
|
+
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
69
|
+
{ name: 'off', value: '/subagent off', desc: d('commands.subagentOff') },
|
|
70
|
+
{ name: 'status', value: '/subagent status', desc: d('commands.subagentStatus') },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
66
73
|
{
|
|
67
74
|
name: '/project_skill', desc: d('commands.skill'), children: [
|
|
68
75
|
{ name: 'toggle', value: '/project_skill', desc: d('commands.toggle') },
|
|
@@ -237,28 +244,51 @@ function renderContextBarInline(history) {
|
|
|
237
244
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
238
245
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
|
|
239
246
|
}
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
// 宿主侧记录已结束轮次最后看到的 plan。notes.md 仍完整保留,只抑制未变化的旧 plan 状态栏,
|
|
248
|
+
// 避免 agent 忘记把 `## Plan:` 改成 `## Done:` 时输入框上方永久悬挂。
|
|
249
|
+
let settledPlanFingerprint;
|
|
250
|
+
/** 读取 notes.md 中唯一活跃的 `## Plan:` 段。进度只统计该段,避免其他笔记 checkbox 污染计数。 */
|
|
251
|
+
function readPlanStatusFromNotes() {
|
|
242
252
|
const sessionId = getCurrentSessionId();
|
|
243
253
|
if (!sessionId)
|
|
244
|
-
return
|
|
254
|
+
return null;
|
|
245
255
|
const root = getSandboxRoot() ?? process.cwd();
|
|
246
256
|
const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
|
|
247
257
|
try {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
258
|
+
const normalized = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
259
|
+
const lines = normalized.split('\n');
|
|
260
|
+
const start = lines.findIndex((line) => /^## Plan:\s*.+$/.test(line));
|
|
261
|
+
if (start < 0)
|
|
262
|
+
return null;
|
|
263
|
+
const endOffset = lines.slice(start + 1).findIndex((line) => /^##\s/.test(line));
|
|
264
|
+
const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
|
|
265
|
+
const section = lines.slice(start, end).join('\n').trimEnd();
|
|
266
|
+
const title = lines[start].match(/^## Plan:\s*(.+)$/)?.[1].trim();
|
|
250
267
|
if (!title)
|
|
251
|
-
return
|
|
252
|
-
const total = (
|
|
253
|
-
const done = (
|
|
254
|
-
const current =
|
|
268
|
+
return null;
|
|
269
|
+
const total = (section.match(/^\s*-\s*\[[ xX]\]\s*\d+\./gm) || []).length;
|
|
270
|
+
const done = (section.match(/^\s*-\s*\[[xX]\]\s*\d+\./gm) || []).length;
|
|
271
|
+
const current = section.match(/^\s*-\s*\[ \]\s*\d+\.\s*(.+)$/m)?.[1].trim();
|
|
255
272
|
const summary = `plan: ${title} (${done}/${total})`;
|
|
256
|
-
|
|
273
|
+
// mtime 让“相同内容被重写为一项新计划”也能重新出现,而不被旧轮次误抑制。
|
|
274
|
+
const fingerprint = `${sessionId}\0${fs.statSync(p).mtimeMs}\0${section}`;
|
|
275
|
+
return { fingerprint, summary: current ? `${summary} ▸ ${current}` : summary };
|
|
257
276
|
}
|
|
258
277
|
catch {
|
|
259
|
-
return
|
|
278
|
+
return null;
|
|
260
279
|
}
|
|
261
280
|
}
|
|
281
|
+
/** 将当前 plan 标记为已结算。只影响状态栏,不修改 agent 的工作笔记。 */
|
|
282
|
+
function settlePlanStatus() {
|
|
283
|
+
settledPlanFingerprint = readPlanStatusFromNotes()?.fingerprint;
|
|
284
|
+
}
|
|
285
|
+
/** 从 notes.md 读取活跃 plan 摘要;已结算且未变化的旧 plan 不再显示。 */
|
|
286
|
+
function readPlanFromNotes() {
|
|
287
|
+
const plan = readPlanStatusFromNotes();
|
|
288
|
+
if (!plan || plan.fingerprint === settledPlanFingerprint)
|
|
289
|
+
return '';
|
|
290
|
+
return plan.summary;
|
|
291
|
+
}
|
|
262
292
|
/** 状态行基线:模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token。repl 在轮次边界、切模式、plan 变更时调。 */
|
|
263
293
|
function refreshStatusBase(history, lastTurnUsage) {
|
|
264
294
|
layout.setStatusBase({
|
|
@@ -299,6 +329,8 @@ function runningStateFor(cmd) {
|
|
|
299
329
|
return { status: t('running.memory'), placeholder: t('running.switching') };
|
|
300
330
|
case '/memory_status':
|
|
301
331
|
return { status: t('running.memoryStatus'), placeholder: '…' };
|
|
332
|
+
case '/subagent':
|
|
333
|
+
return { status: t('running.subagent'), placeholder: t('running.switching') };
|
|
302
334
|
case '/project_skill':
|
|
303
335
|
return { status: t('running.skill'), placeholder: t('running.processing') };
|
|
304
336
|
case '/snapshot':
|
|
@@ -536,6 +568,13 @@ function textOf(c) {
|
|
|
536
568
|
}
|
|
537
569
|
return String(c);
|
|
538
570
|
}
|
|
571
|
+
/** 从旧 session 的消息历史回填输入历史;新 session 使用独立 queryHistory,避免混入合成 user 消息。 */
|
|
572
|
+
function queryHistoryFromMessages(messages) {
|
|
573
|
+
return messages
|
|
574
|
+
.filter((message) => message.role === 'user')
|
|
575
|
+
.map((message) => textOf(message.content))
|
|
576
|
+
.filter((query) => query.trim().length > 0);
|
|
577
|
+
}
|
|
539
578
|
/**
|
|
540
579
|
* 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文):
|
|
541
580
|
* user→❯ 回显、assistant→正文(+ tool_calls 折叠成 ● 摘要行)、tool→↳ 结果预览;system 跳过。
|
|
@@ -649,7 +688,7 @@ export function renderHistory(history) {
|
|
|
649
688
|
* contentWrite 落入内容区(滚动区域内自动滚动,底栏不动)。history 由本模块持有,在轮次间持久;
|
|
650
689
|
* agent 只读取并追加(+ 经 session/ 压缩)。每轮成功结束后自动落盘,退出后可用 --resume / /resume 续接。
|
|
651
690
|
*/
|
|
652
|
-
export async function startRepl(initialHistory, sessionId, updateNotice = null, sandboxRootOverride) {
|
|
691
|
+
export async function startRepl(initialHistory, sessionId, updateNotice = null, sandboxRootOverride, initialQueryHistory) {
|
|
653
692
|
// 模式重置:agentMode 不落盘,每个 REPL 会话从 auto 开始(/resume / --resume 亦重置)。
|
|
654
693
|
setAgentMode('auto');
|
|
655
694
|
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
@@ -686,6 +725,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
686
725
|
const history = initialHistory && initialHistory.length
|
|
687
726
|
? initialHistory
|
|
688
727
|
: [{ role: 'system', content: buildSystemMessage(false) }];
|
|
728
|
+
// 新 session 使用独立输入历史;旧 session 没有该字段时从 user 消息兼容回填一次。
|
|
729
|
+
let queryHistory = initialQueryHistory
|
|
730
|
+
? [...initialQueryHistory]
|
|
731
|
+
: queryHistoryFromMessages(history);
|
|
689
732
|
if (initialHistory &&
|
|
690
733
|
initialHistory.length &&
|
|
691
734
|
history[0]?.role === 'system') {
|
|
@@ -700,12 +743,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
700
743
|
// 本轮 token 累计:runAgent 返回后写入,供底栏模式 chip 右边显示。undefined=无实测
|
|
701
744
|
// (后端不开 include_usage / 后端失败时)。
|
|
702
745
|
let lastTurnUsage;
|
|
703
|
-
const toolsLine = tools.map((t) => t.name).join(' · ');
|
|
704
746
|
const banner = () => ({
|
|
705
747
|
model: config.model,
|
|
706
748
|
baseURL: config.baseURL,
|
|
707
749
|
cwd: process.cwd(),
|
|
708
|
-
tools:
|
|
750
|
+
tools: chatTools.map((tool) => tool.function.name).join(' · '),
|
|
709
751
|
memoryEnabled: isMemoryEnabled(),
|
|
710
752
|
});
|
|
711
753
|
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
@@ -839,7 +881,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
839
881
|
currentSessionId = newSessionId();
|
|
840
882
|
setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
|
|
841
883
|
try {
|
|
842
|
-
saveSession(history, currentSessionId);
|
|
884
|
+
saveSession(history, currentSessionId, queryHistory);
|
|
843
885
|
}
|
|
844
886
|
catch {
|
|
845
887
|
// 落盘失败不阻断
|
|
@@ -896,16 +938,15 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
896
938
|
layout.drawStatusBar();
|
|
897
939
|
});
|
|
898
940
|
// 本轮 token 累计(底栏模式 chip 右边显示)。undefined = 后端不开 include_usage。
|
|
941
|
+
// 状态栏统一在 finally 刷新,确保正常、中断、异常都经过同一 plan 收尾路径。
|
|
899
942
|
lastTurnUsage = result.usage;
|
|
900
|
-
refreshStatusBase(history, lastTurnUsage); // 即时刷状态行显示本轮 token chip
|
|
901
|
-
layout.drawStatusBar();
|
|
902
943
|
ok = !signal.aborted; // 中断(Ctrl+C)→ runAgent 已还原 history,ok=false 不弹审批
|
|
903
944
|
// 成功轮次自动落盘(崩溃也保住上一轮);新会话首轮分配 id
|
|
904
945
|
if (!currentSessionId)
|
|
905
946
|
currentSessionId = newSessionId();
|
|
906
947
|
setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
|
|
907
948
|
try {
|
|
908
|
-
saveSession(history, currentSessionId);
|
|
949
|
+
saveSession(history, currentSessionId, queryHistory);
|
|
909
950
|
}
|
|
910
951
|
catch {
|
|
911
952
|
// 落盘失败不阻断 REPL
|
|
@@ -920,6 +961,16 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
920
961
|
}
|
|
921
962
|
catch (e) {
|
|
922
963
|
ok = false;
|
|
964
|
+
// 请求失败也保存已确认提交的 query,确保立即退出后仍可通过 ↑ 或 resume 找回。
|
|
965
|
+
if (!currentSessionId)
|
|
966
|
+
currentSessionId = newSessionId();
|
|
967
|
+
setCurrentSessionId(currentSessionId, process.cwd());
|
|
968
|
+
try {
|
|
969
|
+
saveSession(history, currentSessionId, queryHistory);
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// 落盘失败不覆盖原始请求错误
|
|
973
|
+
}
|
|
923
974
|
// 多模态相关错误友好提示:OpenAI/Anthropic 等会报 "does not support image" / "vision" / "multimodal" 等关键词,
|
|
924
975
|
// 直接给原文对中文用户不友好。这里翻译成中文 + 提示 /model 换视觉模型。
|
|
925
976
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -937,6 +988,13 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
937
988
|
}
|
|
938
989
|
finally {
|
|
939
990
|
stopRunningListener();
|
|
991
|
+
// 纯 plan 轮正常结束后仍需等待审批/细化,继续展示;其余终态统一结算。
|
|
992
|
+
// 结算只隐藏当前 fingerprint,不修改 notes;后续内容或 mtime 变化会自动重新显示。
|
|
993
|
+
const waitingForPlanApproval = ok && planMode && getAgentMode() === 'plan';
|
|
994
|
+
if (!waitingForPlanApproval)
|
|
995
|
+
settlePlanStatus();
|
|
996
|
+
refreshStatusBase(history, lastTurnUsage);
|
|
997
|
+
layout.drawStatusBar();
|
|
940
998
|
}
|
|
941
999
|
layout.contentWrite('\n'); // 轮次之间空行
|
|
942
1000
|
return ok;
|
|
@@ -955,6 +1013,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
955
1013
|
}
|
|
956
1014
|
history.length = 0;
|
|
957
1015
|
history.push(...loaded.history);
|
|
1016
|
+
queryHistory = loaded.queryHistory
|
|
1017
|
+
? [...loaded.queryHistory]
|
|
1018
|
+
: queryHistoryFromMessages(loaded.history);
|
|
958
1019
|
setAgentMode('auto'); // 续接重置为 auto(mode 不落盘;listener 重写 history[0] 回 auto,与 loaded 幂等)
|
|
959
1020
|
currentSessionId = loaded.id;
|
|
960
1021
|
setCurrentSessionId(loaded.id, process.cwd()); // 切换会话:确保该会话的 notes.md 存在
|
|
@@ -985,6 +1046,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
985
1046
|
input = await promptWithSlashMenu({
|
|
986
1047
|
prompt: PROMPT,
|
|
987
1048
|
commands: buildSlashCommands(),
|
|
1049
|
+
queryHistory,
|
|
988
1050
|
onCycleMode: cycleMode,
|
|
989
1051
|
// /rollback 预填优先;否则上一轮运行中 typeahead 打的字 → 预填进输入框,用户可改可发
|
|
990
1052
|
...(pendingPrefill
|
|
@@ -1804,6 +1866,32 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1804
1866
|
await rollbackFlow();
|
|
1805
1867
|
continue;
|
|
1806
1868
|
}
|
|
1869
|
+
if (line === '/subagent' || line.startsWith('/subagent ')) {
|
|
1870
|
+
const arg = line.startsWith('/subagent ')
|
|
1871
|
+
? line.slice('/subagent '.length).trim().toLowerCase()
|
|
1872
|
+
: 'status';
|
|
1873
|
+
if (arg === '' || arg === 'status') {
|
|
1874
|
+
const enabled = isSubAgentEnabled();
|
|
1875
|
+
const state = t(enabled ? 'subagent.stateOn' : 'subagent.stateOff');
|
|
1876
|
+
layout.contentWrite(`${ui.accent}${t('subagent.status', { state })}${ui.reset}\n` +
|
|
1877
|
+
`${ui.dim}MOCODE_SUBAGENT_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
1881
|
+
layout.contentWrite(`${ui.yellow}${t('subagent.usage')}${ui.reset}\n`);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const enabled = arg === 'on';
|
|
1885
|
+
if (enabled !== isSubAgentEnabled()) {
|
|
1886
|
+
updateSubAgentConfig(enabled);
|
|
1887
|
+
updateConfigKey('MOCODE_SUBAGENT_ENABLED', enabled ? 'true' : 'false');
|
|
1888
|
+
refreshChatTools();
|
|
1889
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1890
|
+
layout.rewriteBanner(bannerLines(banner()));
|
|
1891
|
+
}
|
|
1892
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'subagent.changedOn' : 'subagent.changedOff')}${ui.reset}\n`);
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1807
1895
|
if (line === '/memory_switch' ||
|
|
1808
1896
|
line.startsWith('/memory_switch ') ||
|
|
1809
1897
|
line === '/memory_status' ||
|
|
@@ -2085,6 +2173,8 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
2085
2173
|
layout.enterInputMode(t('repl.idle'));
|
|
2086
2174
|
continue;
|
|
2087
2175
|
}
|
|
2176
|
+
// 只记录已过撤回窗口的真实用户 query;slash 命令和合成执行轮不会走到这里。
|
|
2177
|
+
queryHistory.push(joined);
|
|
2088
2178
|
const initialPlan = getAgentMode() === 'plan'; // 轮首模式(在 runTurn 之前读)
|
|
2089
2179
|
const ok = await runTurn(joined, initialPlan, placeholder);
|
|
2090
2180
|
// plan 轮正常结束(未中断 / 未抛错)→ 看轮末模式决定:
|
package/dist/rollback/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { toText } from '../context/utils.js';
|
|
|
6
6
|
let turnIdCounter = 0;
|
|
7
7
|
let currentTurnId = 0;
|
|
8
8
|
let sequenceCounter = 0;
|
|
9
|
+
/** Monotonic process-local generation; repeated writes to one path still invalidate validation. */
|
|
10
|
+
let mutationVersion = 0;
|
|
9
11
|
let turns = [];
|
|
10
12
|
let snapshots = [];
|
|
11
13
|
const rootDir = () => path.resolve(process.cwd());
|
|
@@ -128,17 +130,22 @@ export function endPathMutation(capture, op) {
|
|
|
128
130
|
const full = safeFullPath(capture.path);
|
|
129
131
|
if (!full)
|
|
130
132
|
return;
|
|
133
|
+
let changed = false;
|
|
131
134
|
const after = readState(full);
|
|
132
135
|
if (!sameState(capture.before, after)) {
|
|
136
|
+
changed = true;
|
|
133
137
|
addSnapshot(snapshotFromState(capture.path, capture.before, capture.sequence, op, capture.createdParents));
|
|
134
138
|
}
|
|
135
139
|
// write_file 会递归创建父目录;即使最终写文件失败,这些目录也是本轮真实副作用。
|
|
136
140
|
for (const parentRel of capture.createdParents) {
|
|
137
141
|
const parent = safeFullPath(parentRel);
|
|
138
142
|
if (parent && readState(parent).kind !== 'missing') {
|
|
143
|
+
changed = true;
|
|
139
144
|
addSnapshot(snapshotFromState(parentRel, { kind: 'missing' }, capture.sequence, op));
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
if (changed)
|
|
148
|
+
mutationVersion += 1;
|
|
142
149
|
}
|
|
143
150
|
function isWorkspaceExcluded(full) {
|
|
144
151
|
const base = path.basename(full).toLowerCase();
|
|
@@ -183,13 +190,37 @@ export function beginWorkspaceMutation() {
|
|
|
183
190
|
export function endWorkspaceMutation(capture, op) {
|
|
184
191
|
const after = scanWorkspace();
|
|
185
192
|
const paths = new Set([...capture.entries.keys(), ...after.keys()]);
|
|
193
|
+
let changed = false;
|
|
186
194
|
for (const rel of paths) {
|
|
187
195
|
const beforeState = capture.entries.get(rel) ?? { kind: 'missing' };
|
|
188
196
|
const afterState = after.get(rel) ?? { kind: 'missing' };
|
|
189
197
|
if (sameState(beforeState, afterState))
|
|
190
198
|
continue;
|
|
199
|
+
changed = true;
|
|
191
200
|
addSnapshot(snapshotFromState(rel, beforeState, capture.sequence, op));
|
|
192
201
|
}
|
|
202
|
+
if (changed)
|
|
203
|
+
mutationVersion += 1;
|
|
204
|
+
}
|
|
205
|
+
/** Current main turn changes, deduplicated by path, plus a generation for validation invalidation. */
|
|
206
|
+
export function getCurrentTurnMutationState() {
|
|
207
|
+
const order = [];
|
|
208
|
+
const byPath = new Map();
|
|
209
|
+
for (const snapshot of snapshots) {
|
|
210
|
+
if (snapshot.turnId !== currentTurnId)
|
|
211
|
+
continue;
|
|
212
|
+
let change = byPath.get(snapshot.path);
|
|
213
|
+
if (!change) {
|
|
214
|
+
change = { path: snapshot.path, ops: [], snapshotAvailable: true };
|
|
215
|
+
byPath.set(snapshot.path, change);
|
|
216
|
+
order.push(snapshot.path);
|
|
217
|
+
}
|
|
218
|
+
for (const op of snapshot.ops ?? ['file_change']) {
|
|
219
|
+
if (!change.ops.includes(op))
|
|
220
|
+
change.ops.push(op);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { version: mutationVersion, changedFiles: order.map((item) => byPath.get(item)) };
|
|
193
224
|
}
|
|
194
225
|
export function listTurns() {
|
|
195
226
|
return turns.slice();
|
package/dist/session/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
|
|
|
12
12
|
export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
|
|
13
13
|
export { dropContextFromHistory, formatDropResult, } from './drop.js';
|
|
14
14
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
|
|
15
|
+
export { appendCurrentSessionTrace } from './trace.js';
|
package/dist/session/persist.js
CHANGED
|
@@ -44,22 +44,27 @@ function firstUserOf(history) {
|
|
|
44
44
|
function sessionPath(id) {
|
|
45
45
|
return path.join(config.sessionDir, id, 'session.json');
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
export function saveSession(history, id) {
|
|
47
|
+
/** 保存会话到磁盘。全新且没有 query 的会话不创建文件;已有会话即使回滚为空也必须覆盖旧记录。 */
|
|
48
|
+
export function saveSession(history, id, queryHistory = []) {
|
|
49
49
|
const meta = {
|
|
50
50
|
id,
|
|
51
51
|
createdAt: idToIso(id),
|
|
52
52
|
model: config.model,
|
|
53
|
-
firstUser: history.length > 1
|
|
53
|
+
firstUser: history.length > 1
|
|
54
|
+
? firstUserOf(history)
|
|
55
|
+
: truncateDisplay((queryHistory[0] ?? '').replace(/\n/g, ' ').trim(), 40),
|
|
54
56
|
};
|
|
55
57
|
const currentPath = sessionPath(id);
|
|
56
58
|
const legacyPath = path.join(config.sessionDir, `${id}.json`);
|
|
57
|
-
if (history.length <= 1 &&
|
|
59
|
+
if (history.length <= 1 &&
|
|
60
|
+
queryHistory.length === 0 &&
|
|
61
|
+
!existsSync(currentPath) &&
|
|
62
|
+
!existsSync(legacyPath)) {
|
|
58
63
|
return meta;
|
|
59
64
|
}
|
|
60
65
|
const dir = path.join(config.sessionDir, id);
|
|
61
66
|
mkdirSync(dir, { recursive: true });
|
|
62
|
-
const record = { ...meta, history };
|
|
67
|
+
const record = { ...meta, history, queryHistory: [...queryHistory] };
|
|
63
68
|
writeFileSync(currentPath, JSON.stringify(record), 'utf8');
|
|
64
69
|
// 一旦写入新式目录,删除旧式扁平副本,避免已回滚消息仍残留在磁盘。
|
|
65
70
|
if (existsSync(legacyPath))
|
|
@@ -86,6 +91,9 @@ export function loadSession(id) {
|
|
|
86
91
|
model: rec.model ?? '',
|
|
87
92
|
firstUser: rec.firstUser ?? '',
|
|
88
93
|
history: rec.history,
|
|
94
|
+
queryHistory: Array.isArray(rec.queryHistory)
|
|
95
|
+
? rec.queryHistory.filter((query) => typeof query === 'string')
|
|
96
|
+
: undefined,
|
|
89
97
|
};
|
|
90
98
|
}
|
|
91
99
|
catch {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
import { getCurrentSessionId } from './state.js';
|
|
5
|
+
function boundedValidation(result) {
|
|
6
|
+
if (!result)
|
|
7
|
+
return undefined;
|
|
8
|
+
const max = 4000;
|
|
9
|
+
return result.output.length <= max
|
|
10
|
+
? result
|
|
11
|
+
: { ...result, output: `${result.output.slice(0, 2000)}\n…[trace output truncated]…\n${result.output.slice(-1900)}` };
|
|
12
|
+
}
|
|
13
|
+
/** Best-effort JSONL trace. Failure must never affect the agent turn. */
|
|
14
|
+
export function appendCurrentSessionTrace(trace) {
|
|
15
|
+
const sessionId = getCurrentSessionId();
|
|
16
|
+
if (!sessionId)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const dir = path.join(config.sessionDir, sessionId);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
appendFileSync(path.join(dir, 'trace.jsonl'), `${JSON.stringify({ ...trace, validation: boundedValidation(trace.validation) })}\n`, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Observability is best-effort and cannot block coding work.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -46,7 +46,33 @@ const _projectSkillEnabledAtBoot = process.env.MOCODE_PROJECT_SKILL === 'true';
|
|
|
46
46
|
const _projectSkillTools = _projectSkillEnabledAtBoot
|
|
47
47
|
? [projectSkillUpdateTool]
|
|
48
48
|
: [];
|
|
49
|
-
|
|
49
|
+
const pathResource = (args) => typeof args.path === 'string' && args.path ? [`file:${args.path}`] : ['workspace'];
|
|
50
|
+
const workspaceResource = () => ['workspace'];
|
|
51
|
+
const memoryResource = () => ['memory-store'];
|
|
52
|
+
const CAPABILITIES = {
|
|
53
|
+
read_file: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: pathResource },
|
|
54
|
+
write_file: { effect: 'write', concurrency: 'resource-locked', retry: 'never', resources: pathResource },
|
|
55
|
+
edit_file: { effect: 'write', concurrency: 'resource-locked', retry: 'never', resources: pathResource },
|
|
56
|
+
run_command: { effect: 'process', concurrency: 'serial', retry: 'never', resources: workspaceResource, supportsAbort: true },
|
|
57
|
+
glob: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
58
|
+
grep: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
59
|
+
codegraph: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource, supportsAbort: true },
|
|
60
|
+
web_search: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
61
|
+
web_fetch: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
62
|
+
use_skill: { effect: 'read', concurrency: 'serial', retry: 'safe' },
|
|
63
|
+
ask_human: { effect: 'read', concurrency: 'serial', retry: 'never' },
|
|
64
|
+
switch_mode: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['agent-mode'] },
|
|
65
|
+
drop_context: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['conversation-context'] },
|
|
66
|
+
memory_save: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
67
|
+
memory_search: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
68
|
+
memory_list: { effect: 'read', concurrency: 'serial', retry: 'safe', resources: memoryResource },
|
|
69
|
+
memory_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
70
|
+
memory_forget: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
71
|
+
project_skill_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: workspaceResource },
|
|
72
|
+
// 子 Agent 共享主工作区;在隔离 workspace / write-set 锁实现前必须串行。
|
|
73
|
+
task: { effect: 'write', concurrency: 'serial', retry: 'never', resources: workspaceResource, supportsAbort: true },
|
|
74
|
+
};
|
|
75
|
+
const rawBuiltinTools = [
|
|
50
76
|
readFileTool,
|
|
51
77
|
writeFileTool,
|
|
52
78
|
editFileTool,
|
|
@@ -58,9 +84,14 @@ export const builtinTools = [
|
|
|
58
84
|
webFetchTool,
|
|
59
85
|
useSkillTool,
|
|
60
86
|
askHumanTool,
|
|
61
|
-
switchModeTool,
|
|
62
|
-
dropContextTool,
|
|
87
|
+
switchModeTool,
|
|
88
|
+
dropContextTool,
|
|
63
89
|
..._memoryTools,
|
|
64
90
|
..._projectSkillTools,
|
|
65
|
-
taskTool,
|
|
91
|
+
taskTool,
|
|
66
92
|
];
|
|
93
|
+
/** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
|
|
94
|
+
export const builtinTools = rawBuiltinTools.map((tool) => ({
|
|
95
|
+
...tool,
|
|
96
|
+
capabilities: CAPABILITIES[tool.name],
|
|
97
|
+
}));
|