mocode-ai 0.4.1 → 0.4.3

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.
@@ -7,7 +7,7 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { chat, planChatTools, } from '../llm/index.js';
9
9
  import { executeTool } from '../tools/registry.js';
10
- import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
10
+ import { getPlanDisabledTools } from '../tools/constants.js';
11
11
  import { getAgentMode, setAgentMode } from './mode.js';
12
12
  import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
13
13
  import { createBudgetScheduler } from '../session/scheduler.js';
@@ -25,6 +25,24 @@ function parseArgs(raw) {
25
25
  return null;
26
26
  }
27
27
  }
28
+ /**
29
+ * Thrashing 检测:同一工具 + 完全相同 arguments 在本轮重复 ≥ THRASH_THRESHOLD 次,
30
+ * 返一段提示(注入到工具结果尾部),引导模型换思路而不是再试一次。
31
+ * 阈值 3 = "试过两次同样的调用还没好,该停了"。指纹 = `${name}\\x00${args}`
32
+ * (直接拼,不哈希——避免热路径开销;args 长度本身有限,内存压力可忽略)。
33
+ * null 表示未触发,不污染输出。
34
+ */
35
+ const THRASH_THRESHOLD = 3;
36
+ function thrashHint(name, args, count) {
37
+ if (count < THRASH_THRESHOLD)
38
+ return null;
39
+ return (`\n\n[hint] This is call #${count} of \`${name}\` with identical arguments — ` +
40
+ 'either failing or returning the same content. STOP retrying and switch strategy:\n' +
41
+ '- read_file / glob → path likely wrong; call `glob` to discover paths, or `ask_human`\n' +
42
+ '- run_command → Windows path-escaping issue; use `read_file` / `glob` with absolute paths instead\n' +
43
+ '- edit_file → old_string mismatch; re-read the file to find the exact text\n' +
44
+ '- otherwise → re-read the tool description; the argument shape may be wrong');
45
+ }
28
46
  /** 只读工具集:一轮多个时,连续的只读工具成组 Promise.all 并行(无副作用、互不依赖)。 */
29
47
  const READ_TOOL_NAMES = new Set([
30
48
  'read_file',
@@ -125,6 +143,31 @@ export async function runAgentCore(opts) {
125
143
  // 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
126
144
  const t0 = Date.now();
127
145
  let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
146
+ // 本轮 token 累计:每步 chat() 返回后把 result.usage 累加,供 onDone 摘要行 + AgentRunResult.usage
147
+ // 透传给 repl(显示在底栏模式 chip 右边)。未开启 include_usage 或全失败时为 undefined。
148
+ let turnUsage;
149
+ const addUsage = (u) => {
150
+ if (!u)
151
+ return;
152
+ turnUsage = turnUsage
153
+ ? {
154
+ promptTokens: turnUsage.promptTokens + u.promptTokens,
155
+ completionTokens: turnUsage.completionTokens + u.completionTokens,
156
+ totalTokens: turnUsage.totalTokens + u.totalTokens,
157
+ cachedTokens: turnUsage.cachedTokens + u.cachedTokens,
158
+ reasoningTokens: turnUsage.reasoningTokens + u.reasoningTokens,
159
+ }
160
+ : u;
161
+ };
162
+ // Thrashing 检测:本轮内同 (name, args) 累计次数。≥3 在工具结果尾部追加 hint(见 thrashHint)。
163
+ // 只在 runAgentCore 内,turn 结束自然 GC;不跨 turn 持久(下一轮重新计数,避免误把历史判为 thrashing)。
164
+ const recentToolCalls = new Map();
165
+ const recordAndHint = (name, args) => {
166
+ const fp = `${name}\x00${args}`;
167
+ const c = (recentToolCalls.get(fp) ?? 0) + 1;
168
+ recentToolCalls.set(fp, c);
169
+ return thrashHint(name, args, c);
170
+ };
128
171
  history.push({ role: 'user', content: userInput });
129
172
  // drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
130
173
  // 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
@@ -210,6 +253,7 @@ export async function runAgentCore(opts) {
210
253
  throw e;
211
254
  }
212
255
  contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
256
+ addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
213
257
  hooks.onChatDone?.(); // 主 agent:spinner.stop()
214
258
  // lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
215
259
  onContextUpdate?.();
@@ -252,7 +296,9 @@ export async function runAgentCore(opts) {
252
296
  const output = await started[k];
253
297
  hooks.onToolDone?.();
254
298
  hooks.onToolResult?.(tc, output, null, null, 1); // 只读工具无 diff
255
- pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
299
+ // Thrashing:history 里附 hint(UI 已用干净 output 渲染,避免屏幕噪声)
300
+ const hint = recordAndHint(tc.name, tc.arguments);
301
+ pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
256
302
  }
257
303
  i = j;
258
304
  }
@@ -272,7 +318,9 @@ export async function runAgentCore(opts) {
272
318
  hooks.onToolHeader?.(tc);
273
319
  const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
274
320
  hooks.onToolResult?.(tc, err, null, null, 1);
275
- pushToolResult(history, tc, err, relprune, lifecycle, scheduler);
321
+ // Thrashing:同上
322
+ const hint = recordAndHint(tc.name, tc.arguments);
323
+ pushToolResult(history, tc, hint ? `${err}${hint}` : err, relprune, lifecycle, scheduler);
276
324
  i++;
277
325
  continue;
278
326
  }
@@ -291,7 +339,9 @@ export async function runAgentCore(opts) {
291
339
  const tc = batch[k];
292
340
  const output = await started[k];
293
341
  hooks.onToolResult?.(tc, output, null, null, 1); // task 结果是摘要,无 diff
294
- pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
342
+ // Thrashing:同上
343
+ const hint = recordAndHint(tc.name, tc.arguments);
344
+ pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
295
345
  }
296
346
  hooks.onToolDone?.();
297
347
  i = j;
@@ -301,11 +351,13 @@ export async function runAgentCore(opts) {
301
351
  const tc = calls[i];
302
352
  // plan 模式防御 backstop:schema 已剔除这些工具,正常不会进这里;防后端幻觉调用——
303
353
  // 不执行,直接返错回灌(让模型看到「plan 模式禁用」并停止),绝不写盘 / 跑命令。
304
- if (getAgentMode() === 'plan' && PLAN_DISABLED_TOOLS.has(tc.name)) {
354
+ if (getAgentMode() === 'plan' && getPlanDisabledTools().has(tc.name)) {
305
355
  hooks.onToolHeader?.(tc);
306
356
  const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
307
357
  hooks.onToolResult?.(tc, err, null, null, 1);
308
- pushToolResult(history, tc, err, relprune, lifecycle, scheduler);
358
+ // Thrashing:同上
359
+ const hint = recordAndHint(tc.name, tc.arguments);
360
+ pushToolResult(history, tc, hint ? `${err}${hint}` : err, relprune, lifecycle, scheduler);
309
361
  i++;
310
362
  continue;
311
363
  }
@@ -318,7 +370,9 @@ export async function runAgentCore(opts) {
318
370
  const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
319
371
  hooks.onToolDone?.();
320
372
  hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
321
- pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
373
+ // Thrashing:同上(history hint,UI 干净)
374
+ const hint = recordAndHint(tc.name, tc.arguments);
375
+ pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
322
376
  // 相关性裁剪 mutation 通知:edit_file/write_file 后,该 path 之前的所有 read_file
323
377
  // 结果已失效(已不再是文件当前状态)→ stub 为存根。pruner 内部 try/catch + 幂等。
324
378
  // 非 mutation 工具(run_command/use_skill/memory_* 等)此处 path="" 不触发。
@@ -346,16 +400,16 @@ export async function runAgentCore(opts) {
346
400
  hooks.onNoReply?.();
347
401
  history.push({ role: 'assistant', content: result.content });
348
402
  done = true;
349
- return { completed: true, finalText: result.content };
403
+ return { completed: true, finalText: result.content, usage: turnUsage };
350
404
  }
351
405
  hooks.onMaxSteps?.();
352
406
  done = true;
353
- return { completed: true, finalText: null };
407
+ return { completed: true, finalText: null, usage: turnUsage };
354
408
  }
355
409
  finally {
356
410
  // 跑完(正常 / 达上限)在回复末尾打耗时摘要行(仿 Claude Code);中断 done=false 不打。
357
411
  if (done) {
358
- hooks.onDone?.(Date.now() - t0);
412
+ hooks.onDone?.(Date.now() - t0, turnUsage);
359
413
  }
360
414
  }
361
415
  }
@@ -110,15 +110,19 @@ onContextUpdate) {
110
110
  layout.contentWrite('\n');
111
111
  layout.contentWrite(`${ui.dim}(已中断)${ui.reset}\n`);
112
112
  },
113
- onDone: (elapsedMs) => layout.contentWrite(` ${ui.dim}✻ Worked for ${fmtElapsed(elapsedMs)}${ui.reset}\n`),
113
+ onDone: (elapsedMs, usage) => {
114
+ const tok = formatTurnTokens(usage);
115
+ layout.contentWrite(` ${ui.dim}✻ Worked for ${fmtElapsed(elapsedMs)}${tok}${ui.reset}\n`);
116
+ },
114
117
  };
115
118
  // 桌宠状态广播:与 TUI hooks 并列注入,互不干扰(petHooks 只调 bridge.sendState,不写屏;
116
119
  // 未 /pet 连接时 sendState 内部 no-op)。仅主 agent 走这里——子 agent(spawn.ts)不引用 createPetHooks,
117
120
  // 故子 agent 永不广播桌宠状态。
118
121
  const petHooks = createPetHooks();
119
122
  const combinedHooks = mergeHooks(hooks, petHooks);
123
+ let result;
120
124
  try {
121
- await runAgentCore({
125
+ result = await runAgentCore({
122
126
  history,
123
127
  userInput,
124
128
  signal,
@@ -129,6 +133,7 @@ onContextUpdate) {
129
133
  finally {
130
134
  spinner.stop();
131
135
  }
136
+ return result;
132
137
  }
133
138
  /** 把两组 AgentHooks 合并为一组:每个方法依次调用两侧已定义的实现(顺序不保证跨方法一致,
134
139
  * 但同一事件内先 a 后 b)。用于把桌宠状态广播 hooks 与 TUI 渲染 hooks 并列挂载,互不影响。 */
@@ -145,3 +150,36 @@ function mergeHooks(a, b) {
145
150
  }
146
151
  return merged;
147
152
  }
153
+ /** 摘要行后追加的本轮 token 文本。
154
+ * 例(无 cache):
155
+ * ` · 1.5k tokens (↑ 1.2k ↓ 0.3k)`
156
+ * 例(命中 cache,DeepSeek 类折扣计费):
157
+ * ` · 72k tokens (↑ 8k ↓ 3k) · ↻ 61k cached`
158
+ * 例(CoT 模型 + cache):
159
+ * ` · 72k tokens (↑ 8k ↓ 3k) · ↻ 61k cached · reasoning 1.2k`
160
+ *
161
+ * 设计要点:
162
+ * - 括号里 ↑ 显示**计费 prompt**(= 全量 - cached),不是后端报的 raw prompt,
163
+ * 否则用户看到 69k 会按全价估成本,实际只花了 5-9k 的 $。
164
+ * - ↻ 显示 cache 命中(白嫖部分),让用户一眼看到优化效果(系统 prompt 越长、对话越长越显著)。
165
+ * - reasoning 是 completion 的子集(已含在 ↓ 里),仅作信息;不二次计入成本。
166
+ * - 总数 total 不变 —— 是数学意义上的"流过的 token",反映 LLM 实际工作量。
167
+ * 关闭 include_usage / 全失败 → usage=undefined → 不输出(保持原摘要行长度)。 */
168
+ function formatTurnTokens(usage) {
169
+ if (!usage)
170
+ return '';
171
+ const total = usage.totalTokens;
172
+ if (!total)
173
+ return '';
174
+ const fmt = (n) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(total >= 10000 ? 0 : 1)}k`);
175
+ const cached = usage.cachedTokens;
176
+ const reasoning = usage.reasoningTokens;
177
+ const billablePrompt = usage.promptTokens - cached;
178
+ const extras = [];
179
+ if (cached > 0)
180
+ extras.push(`↻ ${fmt(cached)} cached`);
181
+ if (reasoning > 0)
182
+ extras.push(`reasoning ${fmt(reasoning)}`);
183
+ const extrasStr = extras.length > 0 ? ` · ${extras.join(' · ')}` : '';
184
+ return ` · ${fmt(total)} tokens (↑ ${fmt(billablePrompt)} ↓ ${fmt(usage.completionTokens)})${extrasStr}`;
185
+ }
@@ -15,19 +15,19 @@
15
15
  // - 逻辑隔离(回滚):skipRollback=true,子 agent 的 write_file/edit_file 改动不进主回滚快照链,
16
16
  // 主 /rollback 不撤销子 agent 改动(靠 git 兜底)。子 agent 与主 agent 共享 cwd(文件改动可见)。
17
17
  import { chatTools } from '../llm/index.js';
18
- import { config } from '../config/index.js';
18
+ import { config, isMemoryEnabled } from '../config/index.js';
19
19
  import { effectiveSystemPrompt } from '../skills/index.js';
20
20
  import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js';
21
21
  import { ui } from '../ui/theme.js';
22
22
  import { runAgentCore } from './core.js';
23
23
  import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
24
24
  /** 子 agent 系统提示后缀:角色与约束。 */
25
- const SUBAGENT_SUFFIX = `
26
-
27
- ## ⛯ SUB-AGENT MODE (you are a sub-agent)
28
- You are a sub-agent spawned by the main agent to handle an isolated sub-task. You have your own conversation history (independent of the main thread).
29
- - Focus solely on the assigned sub-task. Do NOT attempt to call the "task" tool (no recursive spawning).
30
- - Use the tools available to you to complete the sub-task.
25
+ const SUBAGENT_SUFFIX = `
26
+
27
+ ## ⛯ SUB-AGENT MODE (you are a sub-agent)
28
+ You are a sub-agent spawned by the main agent to handle an isolated sub-task. You have your own conversation history (independent of the main thread).
29
+ - Focus solely on the assigned sub-task. Do NOT attempt to call the "task" tool (no recursive spawning).
30
+ - Use the tools available to you to complete the sub-task.
31
31
  - When done, your final text reply will be returned to the main agent as a summary — make it concise and actionable: what you did, key findings, files changed, and any issues. The main agent will decide the next step based on your summary.`;
32
32
  /**
33
33
  * 派生一个子 agent 执行独立子任务。
@@ -44,9 +44,13 @@ You are a sub-agent spawned by the main agent to handle an isolated sub-task. Yo
44
44
  export async function spawnAgent(opts) {
45
45
  const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps ?? 50;
46
46
  // 构造子 agent 系统提示:复用主 agent 组装链 + 子 agent 角色后缀 + 自定义后缀。
47
+ // config.systemPrompt 是 getter(每次访问现拼 buildBasePrompt,反映 isMemoryEnabled),
48
+ // 所以这里直接读 config.systemPrompt 即可;buildMemoryIndexSection 显式按 isMemoryEnabled() 传参,
49
+ // 关闭时该段不进。注意:不能从 spawn.ts 直接 import buildBasePrompt —— 这会
50
+ // 拉起 config → llm → registry → builtins → task → spawn 形成循环求值死锁。
47
51
  const systemPrompt = effectiveSystemPrompt(config.systemPrompt +
48
52
  buildMemorySection() +
49
- buildMemoryIndexSection() +
53
+ buildMemoryIndexSection(isMemoryEnabled()) +
50
54
  SUBAGENT_SUFFIX +
51
55
  (opts.systemPromptSuffix ? `\n\n${opts.systemPromptSuffix}` : ''));
52
56
  // 工具子集:白名单过滤。无白名单 = 全量 chatTools,但始终剔除 task(防递归派生)。
@@ -102,7 +106,12 @@ export async function spawnAgent(opts) {
102
106
  onToolBatchEnd: () => writeBuf('\n'),
103
107
  onNoReply: () => writeBuf(`${ui.dim}(无回复)${ui.reset}\n`),
104
108
  onMaxSteps: () => writeBuf(` ● 达到最大步数(${maxSteps}),子 agent 停止。\n`),
105
- onDone: (elapsedMs) => writeBuf(` ✻ 子 agent 耗时 ${(elapsedMs / 1000).toFixed(1)}s\n`),
109
+ onDone: (elapsedMs, usage) => {
110
+ const tok = usage && usage.totalTokens
111
+ ? ` · ${usage.totalTokens} tokens${usage.cachedTokens ? ` ↻${usage.cachedTokens} cached` : ''}`
112
+ : '';
113
+ writeBuf(` ✻ 子 agent 耗时 ${(elapsedMs / 1000).toFixed(1)}s${tok}\n`);
114
+ },
106
115
  // onStepStart / onChatDone / onToolStart / onToolDone / onAbort:子 agent 静默,无需 spinner / 中断渲染。
107
116
  // abort 还原(history 还原 + 模式还原)由 core 的 abortRestore 处理,hooks 只管展示。
108
117
  };
@@ -59,6 +59,7 @@ const PLATFORM_NOTE = (() => {
59
59
  - You are on Windows; run_command runs commands via cmd.exe (/c). Unix shell builtins are NOT available here.
60
60
  - Windows equivalents: which→where, cat→type, ls→dir, rm→del/rd, cp→copy, mv→move. cmd.exe uses %VAR% (not $VAR); pipes (|) and redirects (>, >>) work, but no $(...) command substitution or backticks.
61
61
  - head/tail/find/grep/sed have no cmd.exe equivalent — use the dedicated tools (read_file for head/tail, glob for find, grep for grep), or invoke PowerShell via run_command if you need more.
62
+ - **Avoid \`run_command\` for file ops on Windows**: cmd /c re-parses paths with backslashes / spaces / quotes — fragile, and ~half of "agent can't find file" failures trace back to this. Use the dedicated tools (read_file/glob/grep) which take absolute Windows paths natively, no shell involved. In particular, NEVER \`dir\` / \`ls\` / \`Test-Path\` / \`if exist\` / \`python -c "os.path.exists(...)"\` — those waste turns on escaping. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string). If you must shell out, use forward slashes (\`C:/foo/bar\`).
62
63
  - Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they're cross-platform and already wired in.`;
63
64
  }
64
65
  if (process.platform === 'darwin') {
@@ -71,27 +72,91 @@ const PLATFORM_NOTE = (() => {
71
72
  - You are on ${process.platform}; run_command runs via bash -c. GNU coreutils — standard POSIX/GNU shell syntax is safe.
72
73
  - Still prefer the dedicated tools (read_file/glob/grep) over hand-rolled shell where they fit — they avoid quoting pitfalls and are already wired in.`;
73
74
  })();
74
- const SYSTEM_PROMPT = `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
75
+ /**
76
+ * 基础系统提示的"记忆段落":开 isMemoryEnabled() 时才拼。
77
+ * 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
78
+ * 都不出现;打开 /memory_switch 后下一次新建 system message 才注入。
79
+ */
80
+ const SYSTEM_PROMPT_MEMORY_SECTION = `
81
+ ## Memory (cross-session long-term facts)
82
+ - A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
83
+ - Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
84
+ - If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
85
+ - Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
86
+ - A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.`;
87
+ /**
88
+ * plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
89
+ * 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
90
+ *
91
+ * memoryEnabled=false 时:memory_save/update/forget 三个写工具名字 + "memory-write tools" 这
92
+ * 句都不出现,且 read-only 列表里的 memory_search/memory_list 也移除——避免提示词里出现
93
+ * 根本不存在的工具名引起 LLM 调不到。
94
+ */
95
+ function buildPlanModeSuffix() {
96
+ if (!isMemoryEnabled()) {
97
+ return `
98
+
99
+ ## ⛯ PLAN MODE (active now)
100
+ You are in PLAN mode: investigate and design only — do NOT execute or change anything.
101
+ - Your editing / command tools (write_file, edit_file, run_command) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human) to investigate.
102
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
103
+ - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
104
+ - When the plan is complete and ready for review, you MUST call the \`ask_human\` tool to surface the plan to the user for approval — do NOT just output the plan as plain text and STOP. ask_human renders a real interactive selection panel inside the TUI; plain-text approval questions in your reply are hard to see and easy to miss.
105
+ - Pass the \`ask_human\` tool a concise plan summary (goal + files/areas to change + key risks + verification) and these three options so the user can decide in one click:
106
+ 1. "按计划执行 (switch to auto and implement)" — user approves; you then call \`switch_mode("auto")\` IN THE SAME turn and proceed.
107
+ 2. "继续细化方案 (stay in plan, refine)" — user wants more detail / alternatives; stay in plan, iterate, and re-ask via \`ask_human\` when ready.
108
+ 3. "取消 / 暂不执行 (abort)" — user wants to stop; STOP, do NOT call \`switch_mode\`.
109
+ - This applies to BOTH paths: whether the user said "先 plan 再 auto" (autonomous) or entered plan mode manually (via /plan or Shift+Tab) for safety review. The single rule is: never silently self-switch and never silently STOP — always route through \`ask_human\` so the user has an explicit chance to approve, refine, or cancel.
110
+ - Do NOT in your text reply ask rhetorical confirmation questions like "shall I proceed? / 是否同意 / 需要你确认吗" — that bypasses the panel and forces the user to type free-text feedback, which is strictly worse than picking from the 3 options. ask_human is the only sanctioned approval channel in plan mode.
111
+ - Note: the REPL may still show its own approval prompt (\`promptIntervention\`) as a defense-in-depth fallback if you somehow STOP without calling ask_human — do not rely on it; the primary path is ask_human.`;
112
+ }
113
+ return `
114
+
115
+ ## ⛯ PLAN MODE (active now)
116
+ You are in PLAN mode: investigate and design only — do NOT execute or change anything.
117
+ - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
118
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
119
+ - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
120
+ - When the plan is complete and ready for review, you MUST call the \`ask_human\` tool to surface the plan to the user for approval — do NOT just output the plan as plain text and STOP. ask_human renders a real interactive selection panel inside the TUI; plain-text approval questions in your reply are hard to see and easy to miss.
121
+ - Pass the \`ask_human\` tool a concise plan summary (goal + files/areas to change + key risks + verification) and these three options so the user can decide in one click:
122
+ 1. "按计划执行 (switch to auto and implement)" — user approves; you then call \`switch_mode("auto")\` IN THE SAME turn and proceed.
123
+ 2. "继续细化方案 (stay in plan, refine)" — user wants more detail / alternatives; stay in plan, iterate, and re-ask via \`ask_human\` when ready.
124
+ 3. "取消 / 暂不执行 (abort)" — user wants to stop; STOP, do NOT call \`switch_mode\`.
125
+ - This applies to BOTH paths: whether the user said "先 plan 再 auto" (autonomous) or entered plan mode manually (via /plan or Shift+Tab) for safety review. The single rule is: never silently self-switch and never silently STOP — always route through \`ask_human\` so the user has an explicit chance to approve, refine, or cancel.
126
+ - Do NOT in your text reply ask rhetorical confirmation questions like "shall I proceed? / 是否同意 / 需要你确认吗" — that bypasses the panel and forces the user to type free-text feedback, which is strictly worse than picking from the 3 options. ask_human is the only sanctioned approval channel in plan mode.
127
+ - Note: the REPL may still show its own approval prompt (\`promptIntervention\`) as a defense-in-depth fallback if you somehow STOP without calling ask_human — do not rely on it; the primary path is ask_human.`;
128
+ }
129
+ /** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
130
+ export function buildBasePrompt() {
131
+ const autoAllToolsLine = isMemoryEnabled()
132
+ ? '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).'
133
+ : '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/web/skills).';
134
+ const memorySection = isMemoryEnabled() ? SYSTEM_PROMPT_MEMORY_SECTION : '';
135
+ const planLine = isMemoryEnabled()
136
+ ? '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.'
137
+ : '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.';
138
+ return `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
75
139
 
76
140
  ${PLATFORM_NOTE}
77
141
 
142
+ ## Step / Turn Economy (read this first — saves LLM calls)
143
+ - **Minimize turns**: each user message costs at least one LLM call, and history grows every step until threshold-triggered compact fires (extra call). If a request contains ≥2 independent sub-goals (e.g. "改 X 然后再优化 Y"), ask the user to split them into separate turns rather than chaining both in one go. State this politely: "这条包含 N 个独立目标,建议拆成 N 次对话,以避免上下文膨胀。"
144
+ - **Batch read-only tools in parallel**: in a single assistant turn, emit multiple tool_calls together — consecutive read-only tools (read_file, glob, grep, codegraph, web_search, web_fetch) auto-execute in parallel. Do NOT call them serially across turns when you could emit them together in one turn. This is the single biggest step-saver.
145
+ - **Decide before reading**: do not read files "just to see"; plan the 2-3 file paths you actually need, then emit them as one batched tool_calls turn.
146
+ - **Don't repeat failed calls**: if the same tool call fails or returns the same content 3 times in this turn, switch strategy (use a different tool, ask the user, or re-read the tool description) — don't keep retrying the same shape.
147
+
78
148
  ## Workflow
79
149
  - Understand before acting: when unsure about requirements or code state, explore first; don't assume.
80
150
  - **Code exploration first action**: before reading files with read_file or searching with grep, check if a .codegraph/ index exists. If it does, use the codegraph tool (explore for questions/features, node for a specific symbol) as your FIRST step — it returns source + call paths in one shot. Only fall back to read_file/grep when codegraph misses, you need just-changed content, or you're editing a known small file. Build the index with \`codegraph init\` if none exists.
81
151
  - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
82
152
  - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
83
153
 
84
- ## Step / Turn Economy (read this — saves LLM calls)
85
- - **Minimize turns**: each user message costs at least one LLM call, and history grows every step until threshold-triggered compact fires (extra call). If a request contains ≥2 independent sub-goals (e.g. "改 X 然后再优化 Y"), ask the user to split them into separate turns rather than chaining both in one go. State this politely: "这条包含 N 个独立目标,建议拆成 N 次对话,以避免上下文膨胀。"
86
- - **Batch read-only tools in parallel**: in a single assistant turn, emit multiple tool_calls together — consecutive read-only tools (read_file, glob, grep, codegraph, web_search, web_fetch) auto-execute in parallel. Do NOT call them serially across turns when you could emit them together in one turn. This is the single biggest step-saver.
87
- - **Decide before reading**: do not read files "just to see"; plan the 2-3 file paths you actually need, then emit them as one batched tool_calls turn.
88
-
89
154
  ## Tool Guidelines
90
155
  - See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
91
156
  - **Prefer codegraph for code exploration**: when understanding/locating code, tracing call chains, or assessing impact of changes, if a .codegraph/ index exists, use the codegraph tool first (explore to query by question, node to look up a single symbol) — it returns relevant source + call paths in one shot, more accurate and economical than piecing together via read_file/grep. Fall back to read_file / grep / glob only when codegraph is unavailable (no index), misses, you need to see just-changed content, or you're editing a single known small file. Build the index first with \`codegraph init\` if none exists.
92
157
  - Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory.
93
158
  - For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
94
- - Use glob to find file paths, grep to search content; don't use run_command to pipe cat / sed / find / grep.
159
+ - Use glob to find file paths, grep to search content. **Don't use run_command for file-level checks** (existence / listing / type) — those have no clean cmd.exe equivalent and Windows path escaping fails often. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string). The earlier rule against \`run_command\` for cat/sed/find/grep still applies.
95
160
  - run_command runs per platform (cmd on Windows, bash elsewhere); state intent before running commands with side effects (deleting files, installing packages, git push, resets, etc.).
96
161
  - Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
97
162
  - Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
@@ -116,16 +181,9 @@ ${PLATFORM_NOTE}
116
181
  - Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
117
182
  - Operate only within authorized scope; when unsure, ask — don't guess.
118
183
 
119
- ## Memory (cross-session long-term facts)
120
- - A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
121
- - Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
122
- - If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
123
- - Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
124
- - A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.
125
-
126
- ## Plan vs Auto modes
127
- - Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).
128
- - For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.
184
+ ${memorySection}
185
+ ${autoAllToolsLine}
186
+ ${planLine}
129
187
 
130
188
  ## Working notepad (todolist) — checklist for complex tasks
131
189
  - For **complex multi-step tasks** (≥3 file changes OR ≥5 tool calls expected OR user says "先计划再执行" / "plan then do" / "按步骤来"), call the \`todolist\` tool FIRST to write a plan to \`.mocode/plans/<id>.md\`, then execute step by step, calling \`todolist update\` to mark progress. For trivial single-step tasks, skip it and just execute.
@@ -137,25 +195,30 @@ ${PLATFORM_NOTE}
137
195
  ## Termination & Reporting
138
196
  - Stop immediately when no more tools are needed; give conclusions directly.
139
197
  - Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.`;
198
+ }
140
199
  /**
141
- * plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
142
- * SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
200
+ * plan 模式追加到系统提示末尾的指令。
201
+ * 历史曾是 `export const PLAN_MODE_SUFFIX`(顶层字面量);现改为按 isMemoryEnabled()
202
+ * 动态拼:false 时不出现 memory_* 工具名,避免 LLM 想调不存在的工具。
203
+ *
204
+ * 注意:已改为 getter(每次访问现拼),让运行时切 /memory_switch 后立即生效。
205
+ * 旧 import `PLAN_MODE_SUFFIX` 路径不变;repl 推荐改用 getPlanModeSuffix()(语义更清晰)。
206
+ * 不能直接 `export const PLAN_MODE_SUFFIX = buildPlanModeSuffix()`:
207
+ * 该表达式在模块初始化时立即求值,而 buildPlanModeSuffix 内部读 config,config 还未求值 → TDZ。
143
208
  */
144
- export const PLAN_MODE_SUFFIX = `
145
-
146
- ## ⛯ PLAN MODE (active now)
147
- You are in PLAN mode: investigate and design only — do NOT execute or change anything.
148
- - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
149
- - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
150
- - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
151
- - Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
152
- - If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
209
+ export function getPlanModeSuffix() {
210
+ return buildPlanModeSuffix();
211
+ }
153
212
  export const config = {
154
213
  baseURL: requireEnv('LLM_BASE_URL'),
155
214
  apiKey: requireEnv('LLM_API_KEY'),
156
215
  model: process.env.LLM_MODEL || 'gpt-4o-mini',
157
216
  maxTokens: process.env.MAX_TOKENS ? Number(process.env.MAX_TOKENS) : undefined,
158
- systemPrompt: SYSTEM_PROMPT,
217
+ // 用 getter 而非 buildBasePrompt() 立即求值:因为本对象字面量求值时 buildBasePrompt 读 config.memoryEnabled,
218
+ // 而 config 还没完成初始化(TDZ)。Getter 让每次访问都现拼,运行时 /memory_switch 立即生效。
219
+ get systemPrompt() {
220
+ return buildBasePrompt();
221
+ },
159
222
  contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || 128000,
160
223
  compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
161
224
  includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
@@ -165,6 +228,7 @@ export const config = {
165
228
  contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
166
229
  contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
167
230
  autoReflect: process.env.AUTO_REFLECT !== 'false',
231
+ memoryEnabled: process.env.MEMORY_ENABLED === 'true',
168
232
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
169
233
  maxSteps: Number(process.env.MAX_STEPS) || 200,
170
234
  subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
@@ -205,3 +269,26 @@ export function updateModelConfig(opts) {
205
269
  process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
206
270
  }
207
271
  }
272
+ /**
273
+ * 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
274
+ * tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
275
+ * 默认 false(新用户零侵入)。
276
+ */
277
+ export function isMemoryEnabled() {
278
+ return config.memoryEnabled;
279
+ }
280
+ /**
281
+ * 切换记忆子系统开关(/memory_switch on|off 调)。
282
+ * - 更新 config 单例字段(其它模块下次调 isMemoryEnabled() 即拿新值)。
283
+ * - 同步 process.env.MEMORY_ENABLED(下次启动 loadEnvFiles 不被文件回填)。
284
+ * 持久化(写 ~/.mocode/config 的 MEMORY_ENABLED 键)由调用方走 writeConfigKeys。
285
+ *
286
+ * 注:开关切换对当前会话的 tool list / 已拼好的 systemPrompt 不会自动重算 —
287
+ * 工具表在 REPL 启动时构建,systemPrompt 在每轮 chat() 拼时按 isMemoryEnabled()
288
+ * 现查现拼(关掉时该轮拼出来的 prompt 即不带 memory_* 段)。所以切换在「下一轮
289
+ * agent 调用」起即时生效,本轮已发出的请求不会回滚。
290
+ */
291
+ export function updateMemoryConfig(enabled) {
292
+ config.memoryEnabled = enabled;
293
+ process.env.MEMORY_ENABLED = enabled ? 'true' : 'false';
294
+ }
package/dist/llm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  import { config } from '../config/index.js';
3
3
  import { tools } from '../tools/registry.js';
4
- import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
4
+ import { getPlanDisabledTools } from '../tools/constants.js';
5
5
  /**
6
6
  * LLM 调用重试策略:
7
7
  * 可重试 → 429 (rate limit) / 5xx (server) / APIConnectionError / Node 网络错 (ETIMEDOUT 等)
@@ -155,10 +155,75 @@ export const chatTools = tools.map((t) => ({
155
155
  },
156
156
  }));
157
157
  /**
158
- * plan 模式用的受限工具 schema:剔除写盘 / 命令 / 记忆写入类(PLAN_DISABLED_TOOLS),
158
+ * plan 模式用的受限工具 schema:剔除写盘 / 命令 / 记忆写入类(getPlanDisabledTools())。
159
159
  * 模型在 plan 模式下只看得到只读工具 → 调不到会改文件的工具。runAgent 在 plan 模式传给 chat()。
160
+ *
161
+ * 注意:planChatTools 是顶层 const(模块初始化时一次性求值);若运行时 /memory_switch 关闭
162
+ * 记忆,这里仍是按当前 isMemoryEnabled() 算出的快照——重启 REPL 才完全生效。
163
+ */
164
+ export const planChatTools = chatTools.filter((t) => !getPlanDisabledTools().has(t.function.name));
165
+ /**
166
+ * 多 provider 兼容的 cache / reasoning 字段提取。
167
+ * 不同后端报 cached 字段名差异巨大,这里按"最常见的几种"顺序 probe,
168
+ * 首个合法非负数字即用(0 也算合法 —— cache miss 是合法状态,不是"无数据")。
169
+ * 字段全缺 → 0,UI 不会显示任何额外标注(零行为变化)。
170
+ *
171
+ * 已实测 / 字段名已知:
172
+ * - OpenAI / Azure / 多数 OpenAI 兼容中转:
173
+ * prompt_tokens_details.cached_tokens
174
+ * - DeepSeek(含 R1):
175
+ * prompt_cache_hit_tokens(扁平,顶层)
176
+ * - Anthropic Claude:
177
+ * cache_read_input_tokens
178
+ * - Moonshot Kimi / GLM-4.6 / Qwen:同 OpenAI 标准
179
+ * - Ollama / 本地 vLLM / 其它:无 usage details → 0(零行为变化)
180
+ *
181
+ * reasoning 字段(CoT 模型):
182
+ * - OpenAI o1 / DeepSeek R1 / GLM-Z1:
183
+ * completion_tokens_details.reasoning_tokens
184
+ * - 其它(个别):reasoning_tokens(顶层)
160
185
  */
161
- export const planChatTools = chatTools.filter((t) => !PLAN_DISABLED_TOOLS.has(t.function.name));
186
+ export function extractUsageExtras(usage) {
187
+ if (!usage || typeof usage !== 'object')
188
+ return { cachedTokens: 0, reasoningTokens: 0 };
189
+ const u = usage;
190
+ // cached probe 顺序:OpenAI 标准 → DeepSeek 扁平 → Anthropic → 杂项兜底
191
+ const cachedCandidates = [
192
+ readPath(u, ['prompt_tokens_details', 'cached_tokens']),
193
+ u.prompt_cache_hit_tokens,
194
+ u.cache_read_input_tokens,
195
+ u.cached_tokens,
196
+ u.prompt_tokens_cached,
197
+ ];
198
+ // reasoning probe 顺序:OpenAI 标准 → 扁平兜底
199
+ const reasoningCandidates = [
200
+ readPath(u, ['completion_tokens_details', 'reasoning_tokens']),
201
+ u.reasoning_tokens,
202
+ ];
203
+ return {
204
+ cachedTokens: firstNumber(cachedCandidates) ?? 0,
205
+ reasoningTokens: firstNumber(reasoningCandidates) ?? 0,
206
+ };
207
+ }
208
+ /** 从对象按路径读嵌套字段(每段都做 null/undefined 检查,任一断即返 undefined)。 */
209
+ function readPath(obj, path) {
210
+ let cur = obj;
211
+ for (const k of path) {
212
+ if (cur == null || typeof cur !== 'object')
213
+ return undefined;
214
+ cur = cur[k];
215
+ }
216
+ return cur;
217
+ }
218
+ /** 从候选数组里取第一个合法非负有限数字(0 算合法 —— cache miss 不是"无数据")。
219
+ * 顺序敏感:数组里前面的 provider 优先(如 OpenAI 标准报 0 时,不会 fallback 到 DeepSeek 字段)。 */
220
+ function firstNumber(arr) {
221
+ for (const v of arr) {
222
+ if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
223
+ return v;
224
+ }
225
+ return undefined;
226
+ }
162
227
  /**
163
228
  * 流式调一次 LLM:增量回调文本,内部累加 tool_calls 片段。
164
229
  * tool_calls 跨 chunk 按 index 累加(id / name / arguments 拼接)。
@@ -219,10 +284,13 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
219
284
  for await (const chunk of stream) {
220
285
  // usage:末尾 chunk(choices 可能为空)在 include_usage 时携带;先读再 continue。
221
286
  if (chunk.usage) {
287
+ const extras = extractUsageExtras(chunk.usage);
222
288
  usage = {
223
289
  promptTokens: chunk.usage.prompt_tokens,
224
290
  completionTokens: chunk.usage.completion_tokens,
225
291
  totalTokens: chunk.usage.total_tokens,
292
+ cachedTokens: extras.cachedTokens,
293
+ reasoningTokens: extras.reasoningTokens,
226
294
  };
227
295
  }
228
296
  const delta = chunk.choices?.[0]?.delta;
@@ -2,6 +2,7 @@
2
2
  // Tier-2:store.ts(叶子,node:fs)做 JSONL CRUD/GC/索引段;reflect.ts(→llm,同 session/)做后台反思 pass。
3
3
  // 被 repl 依赖(注入 systemPrompt + 轮末触发反思 + 退出 drain)。Tier-1 仅依赖 discover.ts。
4
4
  import { loadMemoryFiles } from './discover.js';
5
+ import { isMemoryEnabled } from '../config/index.js';
5
6
  export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
6
7
  export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
7
8
  /** system 消息中 memory 段的字符上限(防过大占窗口——system 在 history[0],compactHistory 不压缩)。 */
@@ -30,8 +31,14 @@ export function loadMemory() {
30
31
  }
31
32
  return cache;
32
33
  }
33
- /** 拼进系统提示的 memory 段;无 memory 返空串(零行为变化)。 */
34
+ /**
35
+ * 拼进系统提示的 memory 段(Tier-1 MOCODE.md);无 memory 返空串(零行为变化)。
36
+ * 记忆子系统总开关关闭(isMemoryEnabled()==false)直接返空串:
37
+ * 提示词、Memory Index 段都不进 — 配合 tools/builtins 把 memory_* 工具屏蔽。
38
+ */
34
39
  export function buildMemorySection() {
40
+ if (!isMemoryEnabled())
41
+ return '';
35
42
  const mem = loadMemory();
36
43
  if (!mem)
37
44
  return '';