mocode-ai 1.1.1 → 1.1.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.
@@ -50,7 +50,7 @@ function parseArgs(raw) {
50
50
  /**
51
51
  * Thrashing 检测:同一工具 + 完全相同 arguments 在本轮重复 ≥ THRASH_THRESHOLD 次,
52
52
  * 返一段提示(注入到工具结果尾部),引导模型换思路而不是再试一次。
53
- * 阈值 3 = "试过两次同样的调用还没好,该停了"。指纹 = `${name}\\x00${args}`
53
+ * 阈值 2 = "试过两次同样的调用还没好,该停了"。指纹 = `${name}\\x00${args}`
54
54
  * (直接拼,不哈希——避免热路径开销;args 长度本身有限,内存压力可忽略)。
55
55
  * null 表示未触发,不污染输出。
56
56
  */
@@ -9,7 +9,7 @@
9
9
  // - 段标题在 buildMocodeCorePrompt 之外,不会被 `## Project context` 索引
10
10
  // 切片误伤;且 buildBasePrompt 注入位置在 ## Workflow 之前,确保 LLM
11
11
  // 先看到纪律再看工具/平台细节。
12
- // - per-model 措辞是"轻量"差异:3 个家族共享 4 阶段结构,只在首句/标签
12
+ // - per-model 措辞是"轻量"差异:3 个家族共享 4 阶段结构,只在首句
13
13
  // 上贴近该家族的指令遵从习惯;真正的 prompt 反演化交给 AHE。
14
14
  // - 语种统一英文:4 份都用同一份核心纪律文本,避免多语种漂移;用户语言
15
15
  // 偏好由现有 i18n 段(assistant.languageInstruction)负责。
@@ -38,6 +38,7 @@ const CORE_SECTION = `## Working discipline — coding tasks (Build-and-Self-Ver
38
38
  Treat "verification" as a first-class part of the task, not an afterthought. Use the smallest evidence-driven loop below.
39
39
 
40
40
  ### Phase 1 — Plan & Discover
41
+ - Open with a one-sentence restatement of your interpretation of the request; if a materially different reading exists, name it briefly before proceeding. This catches misunderstanding before any work is wasted.
41
42
  - State the goal and a concrete acceptance signal, then inspect the relevant code before changing it.
42
43
  - Ask only when an unresolved choice is high-impact or user-owned; otherwise follow repository evidence and proceed.
43
44
 
@@ -52,17 +53,19 @@ Treat "verification" as a first-class part of the task, not an afterthought. Use
52
53
 
53
54
  ### Phase 4 — Fix
54
55
  - Diagnose the root cause, make a focused correction, and rerun the relevant check.
55
- - After three identical failures, change the approach instead of repeating the same call.
56
+ - After two identical failures, change the approach instead of repeating the same call.
56
57
 
57
- **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.`;
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.`;
58
61
  /**
59
- * 把核心段适配到指定 model family:只改首行(语序 / 强动词)与段标题
60
- * 末尾的 [model: X] 标签。Phase 内容保持原样,4 份共享同一份结构化文本。
62
+ * 把核心段适配到指定 model family:只替换首行(语序 / 强动词),段标题
63
+ * 保持原样。Phase 内容保持原样,4 份共享同一份结构化文本。
64
+ * 注意:不再往标题注入 "[model: X]" 标签——它对模型是无意义噪声,
65
+ * 还可能引发自我指涉,反而干扰遵从。
61
66
  */
62
- function adapt(model, opener) {
63
- return CORE_SECTION
64
- .replace('## Working discipline — coding tasks (Build-and-Self-Verify)', `## Working discipline — coding tasks (Build-and-Self-Verify) [model: ${model}]`)
65
- .replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
67
+ function adapt(_model, opener) {
68
+ return CORE_SECTION.replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
66
69
  }
67
70
  /** ASK-01: only user-owned, high-impact choices should interrupt autonomous execution. */
68
71
  const ASK_WHITELIST_SECTION = `## When to ask instead of guess
@@ -70,7 +73,8 @@ const ASK_WHITELIST_SECTION = `## When to ask instead of guess
70
73
  Call \`ask_human\` before coding only when repository evidence cannot resolve a user-owned, high-impact choice:
71
74
  1. irreversible deletion, migration, security, permission, or external side effect;
72
75
  2. public API compatibility (keep, deprecate, rename, or remove);
73
- 3. multiple reasonable options that materially change product behavior.
76
+ 3. multiple reasonable options that materially change product behavior;
77
+ 4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
74
78
 
75
79
  For naming, implementation detail, and verification commands, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
76
80
 
@@ -87,7 +91,7 @@ export function buildWorkDisciplineSection(modelFamily) {
87
91
  section = adapt('anthropic', 'Verification is a hard prerequisite for completion, not a courtesy.');
88
92
  break;
89
93
  case 'openai':
90
- section = adapt('openai', 'Every coding task MUST complete these four phases in order. Skipping or merging phases is treated as a failure.');
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.');
91
95
  break;
92
96
  case 'qwen':
93
97
  section = adapt('qwen', 'Verification is a hard prerequisite for completion; "I wrote the code" is not evidence the code works.');
@@ -117,7 +117,7 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
117
117
  const totalCount = active.length + archived.length;
118
118
  const lines = [
119
119
  '',
120
- `## Session Notepad (${totalCount} section${totalCount === 1 ? '' : 's'} — read \`.mocode/sessions/${sessionId}/notes.md\` to recover full context; surviving compact is the whole point of this file)`,
120
+ `## Session Notepad index (${totalCount} section${totalCount === 1 ? '' : 's'} — read \`.mocode/sessions/${sessionId}/notes.md\` to recover full context; surviving compact is the whole point of this file)`,
121
121
  `Active (${active.length}):`,
122
122
  ...(active.length ? active.map(h => ` - ${h.replace(/^##\s+/, '')}`) : [' - (none)']),
123
123
  ];
@@ -164,7 +164,7 @@ function buildPlanResearchRules() {
164
164
  return `
165
165
  - Locate relevant code and conventions without repeating retrieved work.${cg}
166
166
  - Return an actionable plan with affected files, ordered steps, edge cases, and verification.
167
- - When ready, call \`ask_human\` with exactly: "按计划执行", "继续细化方案", and "取消 / 暂不执行". Approval requires the user to switch to /auto; never execute or switch modes silently.`;
167
+ - When ready, call \`ask_human\` with exactly: "${t('plan.approveOption')}", "${t('plan.refineOption')}", and "${t('plan.cancelOption')}". Approval requires the user to switch to /auto; never execute or switch modes silently.`;
168
168
  }
169
169
  function buildPlanModeSuffix() {
170
170
  return `
@@ -176,7 +176,9 @@ ${buildPlanResearchRules()}`;
176
176
  /** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
177
177
  export function buildBasePrompt(sessionId = getCurrentSessionId()) {
178
178
  const memorySection = buildMemoryPromptSection();
179
- return `## Core behavior
179
+ const notepadSection = buildNotepadSection(sessionId);
180
+ // 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
181
+ const staticBody = `## Core behavior
180
182
  You are mocode, a terminal coding agent. Complete programming tasks through a "think → call tool → observe result → think again" loop until solved. ${t('assistant.languageInstruction')}
181
183
 
182
184
  ## Modes
@@ -189,14 +191,16 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
189
191
 
190
192
  ## Workflow
191
193
  - Use existing conversation and tool evidence before gathering more. Inspect only what supports the next decision; do not guess.
192
- - Keep changes focused. After modifications, run the smallest relevant executable verification and report its result.
194
+ - 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.
193
195
  - Use web search only when freshness materially affects the answer.
194
196
  ${buildCodegraphSection()}
195
197
 
196
198
  ## Tool use
197
199
  - Go directly to a known path or symbol; use discovery tools only when the location is unknown.
198
- - Before an edit, use fresh exact file content and its hash. A mutation, compaction, resume, conflict, or external change makes prior edit context stale.
199
- - Batch only independent calls. Never batch a read with an edit that depends on it; do not repeat overlapping reads or unchanged failed calls.
200
+ - Edit against a FRESH read: before any edit_file/write_file, call read_file on the exact path and copy both its latest hash and the exact target text. Never reconstruct old_string from a grep/summary/diff those lose whitespace and indentation and cause edit failures.
201
+ - A read_file hash from before a compaction, session resume, edit conflict, or external change is STALE and will be rejected re-read rather than reuse an old hash.
202
+ - Emit multiple independent tool calls in ONE assistant message so they run concurrently — e.g. several read_file regions, a grep plus a glob, or several web_fetch calls. One lookup per message wastes a full model round-trip each time. Place parallel-safe calls consecutively; keep any call that depends on their results (e.g. an edit) for the next message.
203
+ - Never batch a read with an edit that depends on it; do not repeat overlapping reads or unchanged failed calls.
200
204
  - On failure, inspect the full error, change the approach, and retry only with a reason. Drop stale tool output when it no longer supports the task.
201
205
  - For generated content over roughly 200 lines or 5K tokens, use small staged writes rather than one oversized tool argument.
202
206
  - Use \`ask_human\` only for a genuinely user-owned decision; otherwise choose the safest reversible option and proceed.
@@ -205,41 +209,59 @@ ${buildCodegraphSection()}
205
209
  - Get confirmation before irreversible or outward-facing actions such as deletion, push, production changes, or external requests, unless explicitly authorized.
206
210
  - Stay within the authorized workspace and disclose anything skipped or unverifiable.
207
211
 
208
- ## Project context (dynamic reference)
209
- ${memorySection}${buildNotepadSection(sessionId)}
210
-
211
- ## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)
212
- Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.
213
-
214
- Keep at most one active plan:
215
- \`\`\`
216
- ## Plan: <title>
217
- Goal: <outcome>
218
- ### Steps
219
- - [ ] 1. <verifiable step>
220
- ### Progress
221
- - <completed phase and evidence>
222
- \`\`\`
223
- 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.
224
-
225
212
  ## Termination & Reporting
226
213
  - Stop immediately when no more tools are needed; give conclusions directly.
227
214
  - **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
228
215
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
229
216
  - 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.`;
217
+ // 动态段(置于末尾):memory 索引 + notepad 目录。仅当有内容才拼
218
+ // "## Project context" 标题,避免空标题噪声(#13)。notepad 使用说明始终保留。
219
+ const dynamicParts = [];
220
+ const ctxContent = `${memorySection}${notepadSection}`.trimEnd();
221
+ if (ctxContent) {
222
+ dynamicParts.push(`## Project context (dynamic reference)\n${ctxContent}`);
223
+ }
224
+ dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
225
+ 'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
226
+ 'Keep at most one active plan:\n' +
227
+ '```\n' +
228
+ '## Plan: <title>\n' +
229
+ 'Goal: <outcome>\n' +
230
+ '### Steps\n' +
231
+ '- [ ] 1. <verifiable step>\n' +
232
+ '### Progress\n' +
233
+ '- <completed phase and evidence>\n' +
234
+ '```\n' +
235
+ '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.');
236
+ return `${staticBody}\n\n${dynamicParts.join('\n\n')}`;
230
237
  }
238
+ /** 静态主体结束 + 会话私有段起点标记,供 buildMocodeCorePrompt 稳健切片(#17)。 */
239
+ const MARKER_STATIC_END = '## Termination & Reporting';
240
+ const MARKER_DYNAMIC_SECTION = '## Project context (dynamic reference)';
241
+ const MARKER_DROPPABLE_SECTION = '## Session Notepad (';
231
242
  /**
232
243
  * Stable, production-grade behavior shared by main and sub agents.
233
- * It intentionally excludes session/project payload (snapshot, memory index, notepad), while
234
- * retaining the exact editing, verification, recovery, safety, and reporting rules.
244
+ * It intentionally excludes the trailing session-specific payload (notepad
245
+ * instructions + dynamic Project context block), while retaining the exact
246
+ * editing, verification, recovery, safety, and reporting rules.
247
+ *
248
+ * 用显式 marker 截取,而非依赖 '## Project context' 字符串的绝对位置——
249
+ * 该标题现在位于 prompt 末尾,且可能缺省(无 memory/无 notepad 时整段不拼,#13),
250
+ * 故以 report 段之后第一个会话私有段标记(memory 索引或 notepad 说明)为切片点,
251
+ * 比旧实现更稳健(#17)。
235
252
  */
236
253
  export function buildMocodeCorePrompt() {
237
254
  const full = buildBasePrompt();
238
- const dynamicStart = full.indexOf('## Project context (dynamic reference)');
239
- const reportingStart = full.indexOf('## Termination & Reporting');
240
- if (dynamicStart < 0 || reportingStart < dynamicStart)
255
+ const reportingStart = full.indexOf(MARKER_STATIC_END);
256
+ if (reportingStart < 0)
241
257
  return full;
242
- return `${full.slice(0, dynamicStart).trimEnd()}\n\n${full.slice(reportingStart)}`;
258
+ const candidateIndices = [MARKER_DYNAMIC_SECTION, MARKER_DROPPABLE_SECTION]
259
+ .map((m) => full.indexOf(m))
260
+ .filter((i) => i > reportingStart);
261
+ if (candidateIndices.length === 0)
262
+ return full; // 无会话私有尾段,整段即静态
263
+ const dropStart = Math.min(...candidateIndices);
264
+ return full.slice(0, dropStart).trimEnd();
243
265
  }
244
266
  /**
245
267
  * plan 模式追加到系统提示末尾的指令。
@@ -27,6 +27,8 @@ export function parseCommand(value) {
27
27
  }
28
28
  if (input.type === 'cancel')
29
29
  return { id: input.id, type: 'cancel' };
30
+ if (input.type === 'compact')
31
+ return { id: input.id, type: 'compact', focus: typeof input.focus === 'string' ? input.focus : undefined };
30
32
  if (input.type === 'approval' && typeof input.approvalId === 'string') {
31
33
  return {
32
34
  id: input.id,
@@ -3,12 +3,13 @@ import readline from 'node:readline';
3
3
  import { runAgentCore } from '../agent/core.js';
4
4
  import { setAgentMode } from '../agent/mode.js';
5
5
  import { buildBasePrompt, config } from '../config/index.js';
6
- import { refreshChatTools } from '../llm/index.js';
6
+ import { refreshChatTools, estimateMessagesTokens } from '../llm/index.js';
7
7
  import { initializeAllMcp, getMcpTools, getMcpWarnings, closeAllMcp } from '../mcp/index.js';
8
8
  import { setSandboxRoot } from '../sandbox/index.js';
9
9
  import { createContextState, loadSession, newSessionId, saveSession } from '../session/index.js';
10
10
  import { setCurrentSessionId } from '../session/state.js';
11
11
  import { buildActiveNotesPlanReminder } from '../session/notes-plan.js';
12
+ import { manualCompact } from '../session/scheduler.js';
12
13
  import { effectiveSystemPrompt } from '../skills/index.js';
13
14
  import { registerToolsExtension } from '../tools/registry.js';
14
15
  import { parseCommand } from './protocol.js';
@@ -139,6 +140,8 @@ async function run(command) {
139
140
  changedFiles: result.changedFiles ?? [],
140
141
  validation: result.validation,
141
142
  usage: result.usage,
143
+ usagePercent: Math.round(contextUsagePercent() * 100),
144
+ contextWindow: config.contextWindowTokens,
142
145
  }, command.id);
143
146
  }
144
147
  catch (cause) {
@@ -172,6 +175,36 @@ function cancel(command) {
172
175
  }
173
176
  emit('cancelling', {}, command.id);
174
177
  }
178
+ /** 计算当前上下文用量百分比(不含 system prompt),用于 UI 展示。 */
179
+ function contextUsagePercent() {
180
+ const dialog = history.filter((m) => m.role !== 'system');
181
+ const est = estimateMessagesTokens(dialog);
182
+ return Math.min(1, est / config.contextWindowTokens);
183
+ }
184
+ async function compact(command) {
185
+ if (activeRun)
186
+ return error('有正在运行的任务,请先取消后再压缩。', command.id);
187
+ try {
188
+ await initializeRuntime();
189
+ if (!sessionId)
190
+ createSession();
191
+ emit('status', { value: 'compacting' }, command.id);
192
+ const log = await manualCompact(history, command.focus, { force: true });
193
+ saveSession(history, sessionId, queryHistory);
194
+ const pct = contextUsagePercent();
195
+ emit('compact_done', {
196
+ compacted: log.compactHistoryCalled,
197
+ beforeTokens: log.compactDetail?.estimateBefore,
198
+ afterTokens: log.compactDetail?.estimateAfter,
199
+ usagePercent: Math.round(pct * 100),
200
+ contextWindow: config.contextWindowTokens,
201
+ }, command.id);
202
+ }
203
+ catch (cause) {
204
+ const message = cause instanceof Error ? cause.message : String(cause);
205
+ error(`压缩失败: ${message}`, command.id);
206
+ }
207
+ }
175
208
  function resolveApproval(command) {
176
209
  const waiter = approvals.get(command.approvalId);
177
210
  if (!waiter)
@@ -184,6 +217,8 @@ async function handle(command) {
184
217
  return run(command);
185
218
  if (command.type === 'cancel')
186
219
  return cancel(command);
220
+ if (command.type === 'compact')
221
+ return compact(command);
187
222
  resolveApproval(command);
188
223
  }
189
224
  const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
@@ -221,6 +221,9 @@ const zhCN = {
221
221
  'plan.running': '执行',
222
222
  'plan.executing': '按计划执行…',
223
223
  'plan.executePrompt': '请按上述计划执行。',
224
+ 'plan.approveOption': '按计划执行',
225
+ 'plan.refineOption': '继续细化方案',
226
+ 'plan.cancelOption': '取消 / 暂不执行',
224
227
  'upgrade.currentVersion': '当前版本:{version}',
225
228
  'upgrade.latestVersion': '最新版本:{version}',
226
229
  'upgrade.noUpdate': '已是最新版本 v{version}',
@@ -463,6 +466,9 @@ const en = {
463
466
  'plan.running': 'Execute',
464
467
  'plan.executing': 'Executing the plan…',
465
468
  'plan.executePrompt': 'Execute the plan above.',
469
+ 'plan.approveOption': 'Execute as planned',
470
+ 'plan.refineOption': 'Refine the plan further',
471
+ 'plan.cancelOption': 'Cancel / hold execution',
466
472
  'upgrade.currentVersion': 'Current version: {version}',
467
473
  'upgrade.latestVersion': 'Latest version: {version}',
468
474
  'upgrade.noUpdate': 'Already up to date: v{version}',
@@ -8,7 +8,8 @@ export const grepTool = {
8
8
  description: 'Search file contents by regex (recursive, excludes node_modules/.git).\n' +
9
9
  'Output: per-file header "<path>: N matches, lines [l1, l2, ...]" + first N matching lines.\n' +
10
10
  'Use the line-number list to call read_file(offset=X, limit=Y) for each region — ' +
11
- 'do NOT read entire files after grepping. For call chains across many files, prefer loading the `codegraph` skill (use_skill).',
11
+ 'do NOT read entire files after grepping. Independent read_file/grep/glob calls may be ' +
12
+ 'issued in the same response and run concurrently. For call chains across many files, prefer loading the `codegraph` skill (use_skill).',
12
13
  parameters: {
13
14
  type: 'object',
14
15
  properties: {
@@ -12,6 +12,8 @@ export const readFileTool = {
12
12
  description: 'Read file content with line numbers. Read before editing.\n' +
13
13
  'For files >500 lines: grep first to locate regions, then call read_file multiple times ' +
14
14
  'with offset+limit (e.g. offset=350, limit=120). Do NOT read an entire large file in one call.\n' +
15
+ 'For files ≤500 lines you may read the whole file in one call. Independent region reads ' +
16
+ 'may be issued in the same response — they run concurrently, saving a round-trip each.\n' +
15
17
  'For architecture or call-chain questions, prefer loading the `codegraph` skill (use_skill) over reading files one at a time.',
16
18
  parameters: {
17
19
  type: 'object',
@@ -20,7 +22,7 @@ export const readFileTool = {
20
22
  offset: { type: 'integer', description: 'Start line, 1-based (default 1).' },
21
23
  limit: {
22
24
  type: 'integer',
23
- description: 'Max lines to read (default 300, hard cap 2000). Keep ranges ~80-200.',
25
+ description: 'Max lines to read (default 300, hard cap 2000). Keep ranges modest (e.g. 80-300); for files ≤500 lines you may read the whole file in one call.',
24
26
  },
25
27
  },
26
28
  required: ['path'],
@@ -141,7 +141,8 @@ function commandOutcome(result) {
141
141
  // ---------- run_command ----------
142
142
  export const runCommandTool = {
143
143
  name: 'run_command',
144
- description: 'Run a shell command, merging stdout+stderr. Default timeout 120s. For tests, builds, git, etc.',
144
+ description: 'Run a shell command, merging stdout+stderr. Default timeout 120s. For tests, builds, git, etc.\n' +
145
+ 'Multiple independent run_command calls may be issued in one response to save model round-trips; they execute serially, so do not depend one on another\'s output within the same message.',
145
146
  risk: 'dangerous',
146
147
  parameters: {
147
148
  type: 'object',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {