mocode-ai 1.1.7 → 1.1.9

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.
Files changed (55) hide show
  1. package/README.md +45 -40
  2. package/README.zh-CN.md +51 -41
  3. package/dist/agent/core.js +137 -447
  4. package/dist/agent/index.js +5 -24
  5. package/dist/agent/spawn.js +5 -5
  6. package/dist/agent/work-discipline.js +16 -70
  7. package/dist/config/index.js +87 -33
  8. package/dist/context/age-aware.js +18 -48
  9. package/dist/context/artifacts.js +19 -17
  10. package/dist/context/budget.js +27 -28
  11. package/dist/context/classifier.js +0 -1
  12. package/dist/context/encoders/index.js +4 -11
  13. package/dist/context/index.js +4 -7
  14. package/dist/context/lifecycle.js +115 -483
  15. package/dist/context/pipeline.js +8 -15
  16. package/dist/context/relevance.js +77 -55
  17. package/dist/host/stdio.js +0 -6
  18. package/dist/i18n/index.js +20 -6
  19. package/dist/index.js +11 -1
  20. package/dist/llm/index.js +82 -9
  21. package/dist/mcp/index.js +0 -1
  22. package/dist/repl/index.js +69 -15
  23. package/dist/runtime/browser-manager.js +299 -0
  24. package/dist/runtime/dev-server-manager.js +354 -0
  25. package/dist/runtime/shutdown.js +26 -0
  26. package/dist/session/compact.js +86 -102
  27. package/dist/session/index.js +0 -1
  28. package/dist/session/notes.js +107 -0
  29. package/dist/session/scheduler.js +88 -92
  30. package/dist/session/trace-metrics.js +5 -92
  31. package/dist/session/trace.js +1 -10
  32. package/dist/tools/builtins/browser.js +199 -0
  33. package/dist/tools/builtins/dev-server.js +99 -0
  34. package/dist/tools/builtins/index.js +33 -19
  35. package/dist/tools/builtins/plan-update.js +144 -0
  36. package/dist/tools/builtins/screenshot.js +173 -0
  37. package/dist/tools/builtins/view-image.js +49 -0
  38. package/dist/tools/constants.js +38 -8
  39. package/dist/tools/registry.js +5 -29
  40. package/dist/ui/batch.js +3 -0
  41. package/dist/ui/content.js +19 -16
  42. package/dist/ui/layout.js +120 -46
  43. package/dist/ui/render.js +11 -0
  44. package/package.json +2 -2
  45. package/dist/agent/middleware/checklist.js +0 -59
  46. package/dist/session/drop.d.ts +0 -19
  47. package/dist/session/drop.js +0 -93
  48. package/dist/tools/builtins/drop-context.d.ts +0 -18
  49. package/dist/tools/builtins/drop-context.js +0 -68
  50. package/dist/verification/diagnostics.js +0 -108
  51. package/dist/verification/fingerprint.js +0 -54
  52. package/dist/verification/index.js +0 -333
  53. package/dist/verification/postconditions.js +0 -98
  54. package/dist/verification/targeted-tests.js +0 -96
  55. package/dist/verification/types.js +0 -1
@@ -15,7 +15,6 @@ import { createPetHooks } from '../pet/state.js';
15
15
  import { t } from '../i18n/index.js';
16
16
  import { isToolErrorOutput } from '../tools/result.js';
17
17
  import { appendCurrentSessionTraceEvent } from '../session/index.js';
18
- import { buildActiveNotesPlanReminder } from '../session/notes-plan.js';
19
18
  /** 当前 turn 的 batch id(runAgent 内闭包变量;一条 turn 一轮 tool batch 结束即清空)。 */
20
19
  let currentBatchId = null;
21
20
  let turnFileChanges = [];
@@ -118,9 +117,9 @@ function flushToolBatch(expandSingleEntry = false) {
118
117
  batch.endBatch(id, layout);
119
118
  if (expandSingleEntry)
120
119
  batch.expandSingleEntryFully(id, layout);
121
- // 普通摘要只有一个“当前空行”,再 break 一次把它提交为分隔空行。
122
- // mutation 自动展开时 content.insertAfter 已先把该当前空行提交到 rows;若这里仍补 \n,
123
- // diff 后就会固定出现两条空白行。
120
+ // 普通批:endBatch 留了 1 个 hasCurrent 空行,break 一次把它提交为分隔空行。
121
+ // mutation 自动展开:expandSingleEntryFully 自己补 separator (\n),这里不能再补 \n,
122
+ // 不然 diff 后面就会出现两条空白行。
124
123
  if (!expandSingleEntry)
125
124
  layout.contentWrite('\n');
126
125
  }
@@ -202,6 +201,8 @@ onContextUpdate) {
202
201
  },
203
202
  onStepStart: () => spinner.start(t('agent.thinking')),
204
203
  onChatDone: () => spinner.stop(),
204
+ // 流式实时用量 → 底栏 context 进度条左侧 chip;轮末由 repl 清空。
205
+ onLiveUsage: (u) => layout.setLiveUsage(u),
205
206
  onTextEnd: () => {
206
207
  if (lastChar && lastChar !== '\n') {
207
208
  layout.contentWrite('\n');
@@ -240,24 +241,6 @@ onContextUpdate) {
240
241
  flushToolBatch();
241
242
  layout.contentWrite(`${ui.dim}${t('agent.aborted')}${ui.reset}\n`);
242
243
  },
243
- onValidationStart: (command) => {
244
- flushToolBatch();
245
- spinner.start(t('agent.validating', { command }));
246
- },
247
- onValidationResult: (validation) => {
248
- spinner.stop();
249
- const color = validation.status === 'passed'
250
- ? ui.green
251
- : validation.status === 'failed'
252
- ? ui.red
253
- : ui.yellow;
254
- const command = validation.command ?? t('agent.validationNoCommand');
255
- const detail = validation.status === 'skipped' && validation.skipReason
256
- ? `${validation.status}: ${validation.skipReason}`
257
- : validation.status;
258
- const symbol = validation.status === 'passed' ? '●' : validation.status === 'failed' ? '×' : '!';
259
- layout.contentWrite(` ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
260
- },
261
244
  onDone: (elapsedMs, usage) => {
262
245
  flushToolBatch();
263
246
  writeChangeOverview();
@@ -281,9 +264,7 @@ onContextUpdate) {
281
264
  userInput,
282
265
  signal,
283
266
  onContextUpdate,
284
- dynamicSystemSuffix: buildActiveNotesPlanReminder,
285
267
  hooks: combinedHooks,
286
- autoValidate: config.autoValidate,
287
268
  onTraceEvent: appendCurrentSessionTraceEvent,
288
269
  });
289
270
  }
@@ -31,8 +31,8 @@ const SUBAGENT_ROLE = `## Sub-agent execution
31
31
  You are executing one delegated sub-task with the same engineering standards and capabilities as mocode.
32
32
  - Treat Task context as authoritative facts already established by the main agent; do not rediscover them without evidence they are stale.
33
33
  - Focus on the delegated scope, but continue until it is genuinely complete. Do not stop to save tokens.
34
- - Do not recursively call sub-agent. A write task runs in an isolated overlay; the coordinator merges and performs final unified verification.
35
- - Return concise findings, changes, verification evidence, and blockers to the coordinator.`;
34
+ - Do not recursively call sub-agent. A write task runs in an isolated overlay; the coordinator merges it safely.
35
+ - Return concise findings, changes, checks you chose to run, and blockers to the coordinator.`;
36
36
  /**
37
37
  * 派生一个子 agent 执行独立子任务。
38
38
  *
@@ -51,7 +51,7 @@ export async function spawnAgent(opts) {
51
51
  summary: null,
52
52
  completed: false,
53
53
  transcript: 'Sub-agent execution is disabled. Enable it with /subagent on.',
54
- status: 'failed', findings: [], readSet: [], changeSet: null, verification: null,
54
+ status: 'failed', findings: [], readSet: [], changeSet: null,
55
55
  usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
56
56
  };
57
57
  }
@@ -68,6 +68,8 @@ export async function spawnAgent(opts) {
68
68
  const requested = opts.tools?.length ? new Set(opts.tools) : null;
69
69
  const readOnly = new Set(['read_file', 'glob', 'grep', 'web_search', 'web_fetch', 'use_skill', 'memory_search', 'memory_list']);
70
70
  toolsOverride = chatTools.filter((tool) => tool.function.name !== 'sub-agent' &&
71
+ // plan_update 直写主会话 notes.md(不走 overlay),子代理不应改动主计划——统一排除。
72
+ tool.function.name !== 'plan_update' &&
71
73
  (!requested || requested.has(tool.function.name)) &&
72
74
  (mode === 'write' || readOnly.has(tool.function.name)));
73
75
  // 独立 history(子 agent 自己持有,不共享主对话)。
@@ -135,7 +137,6 @@ export async function spawnAgent(opts) {
135
137
  maxSteps,
136
138
  toolsOverride,
137
139
  contextState: localContextState,
138
- autoValidate: false,
139
140
  onToolOutcome: (tool, args) => {
140
141
  if (tool === 'read_file' && typeof args.path === 'string')
141
142
  readSet.add(args.path);
@@ -173,7 +174,6 @@ export async function spawnAgent(opts) {
173
174
  findings: result.finalText ? [result.finalText] : [],
174
175
  readSet: [...readSet].sort(),
175
176
  changeSet,
176
- verification: null, // 主 Agent 在所有 coordinator merge 完成后统一验证
177
177
  usage: {
178
178
  promptTokens: result.usage?.promptTokens ?? 0,
179
179
  completionTokens: result.usage?.completionTokens ?? 0,
@@ -1,18 +1,5 @@
1
- // PROMPT-01: Build-and-Self-Verify working discipline.
2
- //
3
- // 把"完成必须验证"作为 system prompt 的一等公民,而不是事后选项。注入一段
4
- // 4 阶段纪律(Plan & Discover → Build → Verify → Fix),并提供 per-model
5
- // 措辞(anthropic / openai / qwen)让 base model 拿到最适合自己的表述。
6
- //
7
- // 关键约束:
8
- // - 纯函数,无副作用,无 config 依赖 → 不踩 TDZ,易测,易回滚。
9
- // - 段标题在 buildMocodeCorePrompt 之外,不会被 `## Project context` 索引
10
- // 切片误伤;且 buildBasePrompt 注入位置在 ## Workflow 之前,确保 LLM
11
- // 先看到纪律再看工具/平台细节。
12
- // - per-model 措辞是"轻量"差异:3 个家族共享 4 阶段结构,只在首句
13
- // 上贴近该家族的指令遵从习惯;真正的 prompt 反演化交给 AHE。
14
- // - 语种统一英文:4 份都用同一份核心纪律文本,避免多语种漂移;用户语言
15
- // 偏好由现有 i18n 段(assistant.languageInstruction)负责。
1
+ // Lightweight, advisory working guidance. The agent decides how much discovery and
2
+ // validation each task needs; the framework does not enforce a completion gate.
16
3
  /**
17
4
  * 从 config.model 字符串里嗅探 model family。匹配规则尽量宽松,够用即可。
18
5
  * 未来 AHE 闭环后可以换成 config.modelFamily 字段。
@@ -33,40 +20,19 @@ export function inferModelFamily(model) {
33
20
  * 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
34
21
  * 标签上做轻量变体。保持短小,详细的完成检查由动态 checklist 按需注入。
35
22
  */
36
- const CORE_SECTION = `## Working discipline — coding tasks (Build-and-Self-Verify)
23
+ const CORE_SECTION = `## Working discipline — coding tasks
37
24
 
38
- Treat "verification" as a first-class part of the task, not an afterthought. Use the smallest evidence-driven loop below.
25
+ Use your judgment to choose the shortest reliable path from the request to a useful result.
39
26
 
40
- ### Phase 1 — Plan & Discover
41
- - Restate the request in one sentence ONLY when it admits two or more materially different readings; name the reading you picked and move on. An unambiguous request gets no restatement — start working. This is the single exception to staying silent during tool-calling turns.
42
- - Settle on the goal and a concrete acceptance signal before inspecting the relevant code; keep them internal unless the user has to weigh in.
43
- - Ask only when an unresolved choice is high-impact or user-owned; otherwise follow repository evidence and proceed.
27
+ - Inspect only the code and context needed for the next decision.
28
+ - Make the smallest coherent change and avoid unrelated refactors.
29
+ - Decide whether validation is useful based on risk, scope, available commands, and the user's request. Validation is optional, not a completion gate.
30
+ - When validation is useful, choose the smallest relevant check yourself; do not run broad test/build suites by default.
31
+ - Re-read or rerun only when evidence is stale or the next edit depends on exact current content.
32
+ - On failure, diagnose before retrying; after repeated identical failures, change approach.
33
+ - Report honestly what you changed, what you checked, and anything left uncertain.
44
34
 
45
- ### Phase 2 — Build
46
- - Make the smallest coherent change; avoid unrelated refactors.
47
- - Add or update a focused test when behavior changes and the project has an applicable test suite.
48
- - Re-read only when a dependent edit needs fresh exact content or state may be stale.
49
-
50
- ### Phase 3 — Verify
51
- - Run the smallest executable check that proves the requested behavior, then read its complete result.
52
- - Compare evidence with the user's request, not merely with the diff.
53
-
54
- ### Phase 4 — Fix
55
- - Diagnose the root cause, make a focused correction, and rerun the relevant check.
56
- - After two identical failures, change the approach instead of repeating the same call.
57
-
58
- **Hard rule (non-negotiable):** "I read the code and it looks right" is not a completion signal. Report the verification performed, or state clearly why it could not be run.
59
-
60
- **Hard rule (non-negotiable):** Never invent file paths, APIs, config keys, flags, or behavior. Every claim about the codebase must trace to tool output in this conversation; explicitly label anything you have not verified as an assumption.`;
61
- /**
62
- * 把核心段适配到指定 model family:只替换首行(语序 / 强动词),段标题
63
- * 保持原样。Phase 内容保持原样,4 份共享同一份结构化文本。
64
- * 注意:不再往标题注入 "[model: X]" 标签——它对模型是无意义噪声,
65
- * 还可能引发自我指涉,反而干扰遵从。
66
- */
67
- function adapt(_model, opener) {
68
- return CORE_SECTION.replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
69
- }
35
+ Never invent file paths, APIs, config keys, flags, or behavior. Distinguish repository evidence from assumptions.`;
70
36
  /** ASK-01: only user-owned, high-impact choices should interrupt autonomous execution. */
71
37
  const ASK_WHITELIST_SECTION = `## When to ask instead of guess
72
38
 
@@ -76,30 +42,10 @@ Call \`ask_human\` before coding only when repository evidence cannot resolve a
76
42
  3. multiple reasonable options that materially change product behavior;
77
43
  4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
78
44
 
79
- For naming, implementation detail, and verification commands, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
45
+ For naming and implementation details, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
80
46
 
81
47
  Budget: at most 2 \`ask_human\` calls per turn. Beyond that, use the safest reversible default and disclose it in the final reply.`;
82
- /**
83
- * 拼出纪律段 + ASK-01 卡点白名单。返回完整段(两段用 \`\\n\\n\` 隔开);
84
- * 工厂之前只返回纪律段,ASK-01 落地后变成纪律 + 白名单两段;
85
- * ASK-01 段是固定英文,不参与 per-model 适配(避免 4 份变体维护成本)。
86
- */
87
- export function buildWorkDisciplineSection(modelFamily) {
88
- let section;
89
- switch (modelFamily) {
90
- case 'anthropic':
91
- section = adapt('anthropic', 'Verification is a hard prerequisite for completion, not a courtesy.');
92
- break;
93
- case 'openai':
94
- section = adapt('openai', 'Every coding task MUST complete these four phases in order. Skipping or merging phases is treated as a failure. For trivial or read-only requests, phases may collapse.');
95
- break;
96
- case 'qwen':
97
- section = adapt('qwen', 'Verification is a hard prerequisite for completion; "I wrote the code" is not evidence the code works.');
98
- break;
99
- case 'other':
100
- case undefined:
101
- default:
102
- section = CORE_SECTION;
103
- }
104
- return `${section}\n\n${ASK_WHITELIST_SECTION}`;
48
+ /** Advisory guidance shared by main and sub-agents. */
49
+ export function buildWorkDisciplineSection(_modelFamily) {
50
+ return `${CORE_SECTION}\n\n${ASK_WHITELIST_SECTION}`;
105
51
  }
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import dotenv from 'dotenv';
5
- import { getSandboxRoot } from '../sandbox/root.js';
6
5
  import { getCurrentSessionId } from '../session/state.js';
6
+ import { getNotesFilePath } from '../session/notes.js';
7
7
  import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
8
8
  import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
9
9
  /**
@@ -41,6 +41,7 @@ export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
41
41
  // 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
42
42
  // 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
43
43
  const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
44
+ export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
44
45
  const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
45
46
  loadEnvFiles();
46
47
  setLanguage(detectLanguage(process.env.MOCODE_LANGUAGE));
@@ -88,11 +89,8 @@ const PLATFORM_NOTE = (() => {
88
89
  * 这样比纯目录列表更显眼,降低 agent 在长上下文里扫过去就忘了的概率。
89
90
  */
90
91
  export function buildNotepadSection(sessionId = getCurrentSessionId()) {
91
- if (!sessionId)
92
- return '';
93
- const root = getSandboxRoot() ?? process.cwd();
94
- const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
95
- if (!fs.existsSync(p))
92
+ const p = getNotesFilePath(sessionId);
93
+ if (!p || !fs.existsSync(p))
96
94
  return '';
97
95
  try {
98
96
  const content = fs.readFileSync(p, 'utf8').trim();
@@ -132,6 +130,55 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
132
130
  return '';
133
131
  }
134
132
  }
133
+ /**
134
+ * 抽取 notes.md 中**唯一活跃**的 `## Plan:` 段原文(含标题行到下一个 `## ` 之前)。
135
+ * 用于 compact 后把计划重注入系统提示,避免 agent 因上下文压缩丢失执行计划。
136
+ * 已结算(`## Done:`)或无 plan 时返回 null。
137
+ */
138
+ export function extractActivePlanSection(sessionId = getCurrentSessionId()) {
139
+ const p = getNotesFilePath(sessionId);
140
+ if (!p || !fs.existsSync(p))
141
+ return null;
142
+ try {
143
+ const normalized = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
144
+ const lines = normalized.split('\n');
145
+ const start = lines.findIndex((l) => /^## Plan:\s*.+$/.test(l));
146
+ if (start < 0)
147
+ return null;
148
+ const endOffset = lines.slice(start + 1).findIndex((l) => /^##\s/.test(l));
149
+ const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
150
+ return lines.slice(start, end).join('\n').trimEnd();
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ /** compact 重注入用的幂等标记:history[0] 中夹住活跃 plan 块,重复注入只替换不累积。 */
157
+ const ACTIVE_PLAN_MARKER = '\n\n<!-- mocode:active-plan -->\n';
158
+ /**
159
+ * 把活跃 `## Plan:` 段重注入系统提示(history[0])。compact 后调用:
160
+ * 若 notes.md 有活跃 plan,则覆盖旧标记块写入最新内容;若无,则清掉残留标记块。
161
+ * 直接改 history[0].content(compact 不破坏 index 0),幂等,返回是否改动。
162
+ */
163
+ export function reinjectActivePlanIntoSystem(history) {
164
+ const sys = history[0];
165
+ if (!sys || sys.role !== 'system' || typeof sys.content !== 'string')
166
+ return false;
167
+ let content = sys.content;
168
+ const markerIdx = content.indexOf(ACTIVE_PLAN_MARKER);
169
+ if (markerIdx >= 0) {
170
+ content = content.slice(0, markerIdx).replace(/\s+$/, '');
171
+ }
172
+ const plan = extractActivePlanSection();
173
+ if (!plan) {
174
+ if (markerIdx < 0)
175
+ return false;
176
+ sys.content = content;
177
+ return true;
178
+ }
179
+ sys.content = `${content}${ACTIVE_PLAN_MARKER}${plan}\n`;
180
+ return true;
181
+ }
135
182
  const SYSTEM_PROMPT_MEMORY_SECTION = `
136
183
  ## Memory (cross-session facts)
137
184
  - The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list.
@@ -195,7 +242,7 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
195
242
 
196
243
  ## Workflow
197
244
  - Use existing conversation and tool evidence before gathering more. Inspect only what supports the next decision; do not guess.
198
- - Keep changes focused. After modifications, run the smallest relevant executable verification that actually exercises the requested behavior, and report its result. A command exiting 0 is not proof the task is done — confirm the specific behavior the user asked for is observed, not merely that the diff applied.
245
+ - Keep changes focused. Decide for yourself whether a check is worth running; prefer the smallest relevant check and avoid broad test/build suites unless the task or risk justifies them.
199
246
  - Use web search only when freshness materially affects the answer.
200
247
  ${buildCodegraphSection()}
201
248
 
@@ -220,31 +267,30 @@ ${buildCodegraphSection()}
220
267
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
221
268
  - 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.
222
269
  ${t('assistant.languageInstruction')}`;
223
- // 动态段(置于末尾):memory 索引 + notepad 目录与使用说明。
224
- // 按需注入(#13):仅当有内容才拼对应标题/说明,避免空标题与无文件时的模板噪声。
225
- // - "## Project context" 仅当 memorySection/notepadSection 非空;
226
- // - notepad 使用说明仅当 notes.md 文件存在(notepadSection 非空)—
227
- // 无文件时连 marker 都不拼,既省 token 也让前缀缓存更稳。core 切片
228
- // 回退到 MARKER_DYNAMIC_SECTION 或整段(见 buildMocodeCorePrompt)。
270
+ // 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
271
+ // 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
272
+ // - "## Project context" 仅当 memorySection/notepadSection 非空(notepad 索引依赖 notes.md 存在);
273
+ // - notepad 使用说明**无条件**注入:否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。说明放在 prompt 末尾,不影响 staticBody 的前缀缓存。
229
274
  const dynamicParts = [];
230
275
  const ctxContent = `${memorySection}${notepadSection}`.trimEnd();
231
276
  if (ctxContent) {
232
277
  dynamicParts.push(`## Project context (dynamic reference)\n${ctxContent}`);
233
278
  }
234
- if (notepadSection) {
235
- dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
236
- 'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
237
- 'Keep at most one active plan:\n' +
238
- '```\n' +
239
- '## Plan: <title>\n' +
240
- 'Goal: <outcome>\n' +
241
- '### Steps\n' +
242
- '- [ ] 1. <verifiable step>\n' +
243
- '### Progress\n' +
244
- '- <completed phase and evidence>\n' +
245
- '```\n' +
246
- 'Update checkboxes and Progress after each completed phase. Before the final reply, reconcile the plan with actual work, then rename it to `## Done:` or remove it. Keep other notes concise and session-specific; use memory for stable cross-session facts.');
247
- }
279
+ dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
280
+ 'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
281
+ 'Record and update the execution plan with the `plan_update` tool (preferred over editing checkboxes by hand); it keeps at most one active plan as a `## Plan:` section:\n' +
282
+ '```\n' +
283
+ '## Plan: <title>\n' +
284
+ 'Goal: <outcome>\n' +
285
+ '### Steps\n' +
286
+ '- [ ] 1. <self-contained step: target file/symbol, the change, and how to verify>\n' +
287
+ '### Progress\n' +
288
+ '- <completed/total>\n' +
289
+ '```\n' +
290
+ 'Keep at most one step in_progress, and mark a step completed as soon as its work is done — do not batch updates to the end of the turn. ' +
291
+ 'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
292
+ 'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
293
+ 'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.');
248
294
  return `${staticBody}\n\n${dynamicParts.join('\n\n')}`;
249
295
  }
250
296
  /** 静态主体结束 + 会话私有段起点标记,供 buildMocodeCorePrompt 稳健切片(#17)。 */
@@ -298,21 +344,20 @@ export const config = {
298
344
  get systemPrompt() {
299
345
  return buildBasePrompt();
300
346
  },
301
- contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || 128000,
302
- compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
347
+ contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || DEFAULT_CONTEXT_WINDOW_TOKENS,
303
348
  includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
304
349
  autoCompact: process.env.AUTO_COMPACT !== 'false',
305
- autoValidate: process.env.MOCODE_AUTO_VALIDATE !== 'false',
306
- contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
307
- contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
350
+ contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE === 'true',
351
+ contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE === 'true',
308
352
  contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
309
353
  contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
310
- autoReflect: process.env.AUTO_REFLECT !== 'false',
354
+ autoReflect: process.env.AUTO_REFLECT === 'true',
311
355
  memoryEnabled: process.env.MEMORY_ENABLED === 'true',
312
356
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
313
357
  maxSteps: Number(process.env.MAX_STEPS) || 1000,
314
358
  subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
315
359
  subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
360
+ frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
316
361
  sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
317
362
  searchApiKey: process.env.ANYSEARCH_API_KEY,
318
363
  sandboxRoot: process.env.SANDBOX_ROOT || undefined,
@@ -361,6 +406,15 @@ export function updateSubAgentConfig(enabled) {
361
406
  config.subAgentEnabled = enabled;
362
407
  process.env.MOCODE_SUBAGENT_ENABLED = enabled ? 'true' : 'false';
363
408
  }
409
+ /** 前端工具簇总开关;默认 false,关闭时 browser/dev_server/screenshot/view_image 不进入模型工具表。 */
410
+ export function isFrontendToolsEnabled() {
411
+ return config.frontendToolsEnabled;
412
+ }
413
+ /** 运行时切换前端工具簇;工具 schema 刷新与持久化由 REPL 调用方完成。 */
414
+ export function updateFrontendToolsConfig(enabled) {
415
+ config.frontendToolsEnabled = enabled;
416
+ process.env.MOCODE_FRONTEND_TOOLS_ENABLED = enabled ? 'true' : 'false';
417
+ }
364
418
  /**
365
419
  * 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
366
420
  * tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
@@ -1,12 +1,8 @@
1
- // Age-aware tool-result encoding coordinator.
2
- // Initial pushes stay conservative; old Cold results are re-encoded before chat.
3
- import { TOOL_OLD_AGE } from './budget.js';
1
+ // Pressure-only tool-result encoding coordinator.
2
+ // Normal pushes are raw apart from the per-result hard safety cap.
4
3
  import { optimizeToolResult } from './pipeline.js';
5
4
  import { canonicalizePath, extractPath, isToolResultSuccess, toText, } from './utils.js';
6
- /**
7
- * Tracks successful first reads and tool-result age without coupling encoders to
8
- * lifecycle's mutable history indexes. All methods are fail-safe and idempotent.
9
- */
5
+ /** Rebuilds records from current history for each pressure pass. */
10
6
  export class AgeAwareEncodingState {
11
7
  pushOrdinal = 0;
12
8
  records = new Map();
@@ -14,36 +10,16 @@ export class AgeAwareEncodingState {
14
10
  constructor(history = []) {
15
11
  this.rehydrate(history);
16
12
  }
17
- /** Build the conservative context for a newly completed tool result. */
18
- preparePush(tc, succeeded) {
19
- const path = tc.name === 'read_file'
20
- ? canonicalizePath(extractPath(tc.arguments))
21
- : null;
22
- const isFirstRead = path ? !this.seenReadPaths.has(path) : undefined;
23
- this.records.set(tc.id, {
24
- toolCallId: tc.id,
25
- toolName: tc.name,
26
- argsRaw: tc.arguments,
27
- pushOrdinal: this.pushOrdinal,
28
- succeeded,
29
- isFirstRead,
30
- agedEncoded: false,
31
- });
32
- this.pushOrdinal++;
33
- // Failed reads must not consume the "first successful read" privilege.
34
- if (succeeded && path)
35
- this.seenReadPaths.add(path);
36
- return {
37
- age: 0,
38
- isCold: false,
39
- isFirstRead,
40
- phase: 'push',
41
- };
42
- }
43
- /** Re-encode eligible tool messages in the Cold prefix in place. */
44
- sweep(history, hotBoundary) {
13
+ /**
14
+ * Pressure-only, progressive encoding of Cold logs and retrievable searches.
15
+ * It never touches code reads, skills, human decisions, or sub-agent output;
16
+ * repeated pressure passes may further reduce content only when strictly shorter.
17
+ */
18
+ sweepPressure(history, hotBoundary) {
45
19
  try {
20
+ const pressureEncodable = new Set(['run_command', 'grep', 'glob', 'web_search', 'web_fetch']);
46
21
  const end = Math.min(Math.max(hotBoundary, 1), history.length);
22
+ let encodedCount = 0;
47
23
  for (let idx = 1; idx < end; idx++) {
48
24
  const message = history[idx];
49
25
  if (message.role !== 'tool')
@@ -51,32 +27,27 @@ export class AgeAwareEncodingState {
51
27
  const toolMessage = message;
52
28
  const id = toolMessage.tool_call_id;
53
29
  const record = id ? this.records.get(id) : undefined;
54
- if (!record || !record.succeeded || record.agedEncoded)
30
+ if (!record || !record.succeeded || !pressureEncodable.has(record.toolName))
55
31
  continue;
56
32
  const content = toText(toolMessage.content);
57
- if (!content || content.startsWith('⌦[')) {
58
- record.agedEncoded = true;
33
+ if (!content || content.startsWith('⌦['))
59
34
  continue;
60
- }
61
- // Exclude the result's own push: immediately after insertion its age is 0.
62
35
  const age = Math.max(0, this.pushOrdinal - record.pushOrdinal - 1);
63
- if (age < TOOL_OLD_AGE)
64
- continue;
65
36
  const encoded = optimizeToolResult(record.toolName, content, record.argsRaw, {
66
37
  age,
67
38
  isCold: true,
68
39
  isFirstRead: record.isFirstRead,
69
40
  phase: 'sweep',
70
41
  });
71
- // Aged encoding is a degradation step: never replace content with a
72
- // representation that is equal-sized or larger.
73
- if (encoded.length < content.length)
42
+ if (encoded.length < content.length) {
74
43
  toolMessage.content = encoded;
75
- record.agedEncoded = true;
44
+ encodedCount++;
45
+ }
76
46
  }
47
+ return encodedCount;
77
48
  }
78
49
  catch {
79
- // Context optimization must never block an agent request.
50
+ return 0;
80
51
  }
81
52
  }
82
53
  /** Rebuild stable state after resume or structural history compaction. */
@@ -119,7 +90,6 @@ export class AgeAwareEncodingState {
119
90
  pushOrdinal: this.pushOrdinal,
120
91
  succeeded,
121
92
  isFirstRead,
122
- agedEncoded: false,
123
93
  });
124
94
  this.pushOrdinal++;
125
95
  if (succeeded && path)
@@ -132,10 +132,17 @@ export function recordArtifact(state, history, idx, output, succeeded) {
132
132
  updateStats(state, stateFor(state));
133
133
  }
134
134
  function affected(artifact, changed) {
135
- return artifact.dependencies.some((dependency) => dependency.path === '*' || changed.has(dependency.path));
135
+ // '*' 依赖(无法解析出具体文件路径的诊断/搜索结果)不与任何具体写操作关联:
136
+ // 任何文件写入都会作废全部 '*' artifact,等于每次 mutation 都销毁
137
+ // git/测试/构建等历史证据,模型被迫反复 re-run,轮次爆炸。只失效路径明确命中的。
138
+ return artifact.dependencies.some((dependency) => dependency.path !== '*' && changed.has(dependency.path));
136
139
  }
137
- /** Mark and immediately stub stale facts; this is stronger than waiting for budget pressure. */
138
- export function invalidateArtifacts(state, history, changedFiles) {
140
+ /**
141
+ * Mark precise, file-backed facts stale without changing the evidence in history.
142
+ * A stale result can still explain a later edit or failure; pressure compression is
143
+ * the only path allowed to replace its content with a compact marker.
144
+ */
145
+ export function invalidateArtifacts(state, _history, changedFiles) {
139
146
  const changed = new Set(changedFiles.map(canonicalizePath).filter((item) => !!item));
140
147
  if (changed.size === 0)
141
148
  return 0;
@@ -145,15 +152,6 @@ export function invalidateArtifacts(state, history, changedFiles) {
145
152
  if (artifact.freshness !== 'fresh' || !affected(artifact, changed))
146
153
  continue;
147
154
  artifact.freshness = 'stale';
148
- const message = history[artifact.messageIndex];
149
- if (message?.role === 'tool') {
150
- const original = toText(message.content);
151
- const paths = artifact.dependencies.map((item) => item.path).join(', ');
152
- const stub = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id} dependencies=${paths} ` +
153
- `invalidated-by=${[...changed].join(', ')}; re-run ${artifact.source.tool} before using this fact.`;
154
- message.content = stub;
155
- artifact.tokenCount = estimateTokens(stub);
156
- }
157
155
  count++;
158
156
  }
159
157
  updateStats(state, artifactState);
@@ -193,7 +191,7 @@ export function rehydrateArtifacts(state, history) {
193
191
  }
194
192
  updateStats(state, artifactState);
195
193
  }
196
- /** Compare captured dependency versions before each model step to detect external edits. */
194
+ /** Compare captured dependency versions before each model step and mark stale metadata only. */
197
195
  export function refreshArtifactFreshness(state, history) {
198
196
  const changed = new Set();
199
197
  for (const artifact of stateFor(state).artifacts.values()) {
@@ -208,19 +206,23 @@ export function refreshArtifactFreshness(state, history) {
208
206
  }
209
207
  return changed.size > 0 ? invalidateArtifacts(state, history, [...changed]) : 0;
210
208
  }
211
- /** Scheduler entry point: stale artifacts are already stubs; normalize any resumed stale message first. */
212
- export function pruneStaleArtifacts(state, history) {
209
+ /**
210
+ * Pressure-only stage: replace stale evidence in the Cold prefix with a compact
211
+ * marker. Recent/current work stays intact, and normal mutation handling never
212
+ * calls this function.
213
+ */
214
+ export function pruneStaleArtifacts(state, history, coldBoundary) {
213
215
  const artifactState = stateFor(state);
214
216
  let pruned = 0;
215
217
  for (const artifact of artifactState.artifacts.values()) {
216
- if (artifact.freshness !== 'stale')
218
+ if (artifact.freshness !== 'stale' || artifact.messageIndex >= coldBoundary)
217
219
  continue;
218
220
  const message = history[artifact.messageIndex];
219
221
  if (message?.role !== 'tool')
220
222
  continue;
221
223
  const content = toText(message.content);
222
224
  if (!content.startsWith(STALE_PREFIX)) {
223
- message.content = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id}; re-run before use.`;
225
+ message.content = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id}; source changed, so this content may be outdated.`;
224
226
  artifact.tokenCount = estimateTokens(String(message.content));
225
227
  pruned++;
226
228
  }