mocode-ai 1.7.4 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -259,7 +259,7 @@ The tables below are selected capabilities, not an exhaustive tool inventory. Cu
259
259
 
260
260
  | Tool | Purpose |
261
261
  | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
262
- | `read_file` | Read a file: text with line numbers (`offset` / `limit`), images (PNG/JPEG/GIF/WebP detected by magic bytes) as visual model input; other binaries are rejected instead of dumped as garbled text |
262
+ | `read_file` | Read a file: text with line numbers (`offset` / `limit`, output capped at ~32k chars per call — size ranges by content density, not line count), images (PNG/JPEG/GIF/WebP detected by magic bytes) as visual model input; other binaries are rejected instead of dumped as garbled text |
263
263
  | `write_file` | Create/overwrite a file, auto-creating parent directories; `append=true` adds to the end without re-sending the whole file |
264
264
  | `edit_file` | Precise string replacement (`old_string` must match uniquely) |
265
265
  | `run_command` | Run a foreground shell command, merging stdout+stderr, 120s default timeout; `shell=cmd\|powershell\|bash` picks the interpreter |
package/README.zh-CN.md CHANGED
@@ -258,7 +258,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
258
258
 
259
259
  | 工具 | 作用 |
260
260
  | ------------- | ---------------------------------------------------------------------------------------------------------------------- |
261
- | `read_file` | 读文件:文本带行号(`offset` / `limit`);PNG/JPEG/GIF/WebP 按**魔数**识别(扩展名会说谎)并作为视觉输入回灌(4 MiB 内联上限,超限 PNG 自动降采样,`detail=low\|high` 控分辨率);其余二进制明确拒绝而非灌乱码 |
261
+ | `read_file` | 读文件:文本带行号(`offset` / `limit`,单次输出上限约 32k 字符——按内容密度规划区间而非行数);PNG/JPEG/GIF/WebP 按**魔数**识别(扩展名会说谎)并作为视觉输入回灌(4 MiB 内联上限,超限 PNG 自动降采样,`detail=low\|high` 控分辨率);其余二进制明确拒绝而非灌乱码 |
262
262
  | `screenshot` | 经用户确认后截取主显示器或整个桌面,保存 PNG 并立即交给视觉模型分析 |
263
263
  | `write_file` | 创建/覆盖文件,自动建父目录;`append=true` 在文件末尾追加,无需重发全文(分段写长文件/记日志的正确姿势) |
264
264
  | `edit_file` | 精确字符串替换(`old_string` 须唯一匹配) |
@@ -693,7 +693,7 @@ export async function compactHistory(history, opts) {
693
693
  }
694
694
  let oldGroups = groups.slice(0, groups.length - kept.length);
695
695
  // 非空 user 兜底:保留区从尾部累积,长回合里最近的若干组常常全是 assistant+tool,
696
- // 一个 8000 字符的工具结果就能吃满小窗口的 keepBudget → user 全落进旧区被摘要掉 →
696
+ // 一个 MAX_HISTORY_RESULT 量级的工具结果就能吃满小窗口的 keepBudget → user 全落进旧区被摘要掉 →
697
697
  // 重建后 history 无 user → 下一步 chat() 被 hasNonEmptyUser 守卫直接拒(整轮中断)。
698
698
  // 把紧邻保留区之前的那条 user 移进保留区:它是当前任务的原始请求,且 user 消息本身极小。
699
699
  // 放这里(而非重建后补 stub)是因为:原文保留,不造新消息形状,也不必进摘要器。
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import { contentHash } from '../../changeset/index.js';
4
- import { IMAGE_READ_MARKER, MAX_FILE_LINES } from '../constants.js';
4
+ import { IMAGE_READ_MARKER, MAX_FILE_LINES, MAX_HISTORY_RESULT } from '../constants.js';
5
5
  import { MAX_INLINE_BYTES_DEFAULT, isProbablyBinary, loadImageAttachmentWithDownscale, sniffImageMime, } from '../../attachments/image.js';
6
6
  import { config } from '../../config/index.js';
7
7
  /** 默认单次 read_file 拉取的行数。刻意压低,逼 LLM 分块读大文件,
@@ -47,7 +47,7 @@ export const readFileTool = {
47
47
  offset: { type: 'integer', description: 'Start line, 1-based (default 1). Text files only; ignored for images.' },
48
48
  limit: {
49
49
  type: 'integer',
50
- 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. Text only; ignored for images.',
50
+ description: 'Max lines to read (default 300, hard cap 2000). Size by OUTPUT, not lines: outputs are capped at ~32k chars per call, so a dense (CJK) file fits ~200-400 lines in one call while sparse code fits 1000+. If a range exceeds that, split it; the output tail always reports how many lines remain. Text only; ignored for images.',
51
51
  },
52
52
  detail: {
53
53
  type: 'string',
@@ -142,10 +142,61 @@ export const readFileTool = {
142
142
  const output = end < lines.length
143
143
  ? `${artifactHeader}\n${body}\n\n... (${lines.length - end} 行未显示,共 ${lines.length} 行)`
144
144
  : `${artifactHeader}\n${body}`;
145
- dedup?.record(path, reqStart, reqEnd, contentHash(data));
145
+ // dedup 只登记「确实会进 context 的区间」。pushToolResult 会把超长输出经
146
+ // capToolResultForHistory 中截到 MAX_HISTORY_RESULT(丢中段)——若此处仍登记
147
+ // 完整 [reqStart, reqEnd],后续对「被截掉的中段」的重读会被 findUnchanged 命中,
148
+ // 模型拿到 unchanged 短指针却找不到内容,只能反复缩小区间撞墙、最终用 shell 绕过
149
+ // (实测事故:会话 20261012-033757)。故输出超 cap 时只登记头/尾两段实际保留的行。
150
+ if (dedup && config.readDedup && output.length > MAX_HISTORY_RESULT) {
151
+ const fileHash = contentHash(data);
152
+ const truncated = truncatedOutputRanges(lines, start, end, output.length, MAX_HISTORY_RESULT);
153
+ for (const r of truncated)
154
+ dedup.record(path, r.start, r.end, fileHash);
155
+ }
156
+ else {
157
+ dedup?.record(path, reqStart, reqEnd, contentHash(data));
158
+ }
146
159
  return { status: 'success', code: 'OK', retryable: false, output };
147
160
  },
148
161
  };
162
+ /**
163
+ * 输出将被中截(头 60% / 尾 40%)时,反推真正保留进 context 的行区间。
164
+ * 输出长度按行聚合估算:行号前缀(≤7 字符 + tab)与换行在各行间均摊,误差 ≤1 行,
165
+ * 对 dedup 区间判定足够保守(宁可少记、不可多记——漏记只会多一次全量返回,多记会造成幽灵命中)。
166
+ */
167
+ function truncatedOutputRanges(lines, start, end, outputLen, cap) {
168
+ const marker = `…[已截断 ${outputLen - cap} 字符]…`;
169
+ const remain = cap - marker.length;
170
+ const headChars = Math.ceil(remain * 0.6);
171
+ const tailChars = remain - headChars;
172
+ // 头段:从 start 起累加行,直至耗尽 headChars(含 artifact header 的固定开销,按保守处理:header 不占行预算)。
173
+ const lineLen = (i) => `${String(i + 1).padStart(6, ' ')}\t${lines[i]}`.length + 1;
174
+ const ranges = [];
175
+ let acc = 0;
176
+ let headEnd = start - 1;
177
+ for (let i = start; i < end; i++) {
178
+ const l = lineLen(i);
179
+ if (acc + l > headChars)
180
+ break;
181
+ acc += l;
182
+ headEnd = i;
183
+ }
184
+ if (headEnd >= start)
185
+ ranges.push({ start: start + 1, end: headEnd + 1 });
186
+ // 尾段:从 end-1 反向累加,直至耗尽 tailChars。区间下限不得超过头段结尾(头尾重叠说明整体保留)。
187
+ acc = 0;
188
+ let tailStart = end;
189
+ for (let i = end - 1; i >= start; i--) {
190
+ const l = lineLen(i);
191
+ if (acc + l > tailChars)
192
+ break;
193
+ acc += l;
194
+ tailStart = i;
195
+ }
196
+ if (tailStart < end && tailStart > headEnd)
197
+ ranges.push({ start: tailStart + 1, end });
198
+ return ranges;
199
+ }
149
200
  /**
150
201
  * 图片分支:走视觉通道(modelAttachments),文本 output 只留一句可读摘要。
151
202
  *
@@ -12,8 +12,13 @@ export const MAX_RESULTS = 100;
12
12
  * 从「N 行」改成「图片已附加」,而 render.ts → builtins → registry 会绕出模块循环。
13
13
  */
14
14
  export const IMAGE_READ_MARKER = '[image]';
15
- /** 进 history 的单条工具结果上限(字符)。push-time 第一层裁剪,保 head + 标记 + tail。 */
16
- export const MAX_HISTORY_RESULT = 8000;
15
+ /**
16
+ * 进 history 的单条工具结果上限(字符)。push-time 第一层裁剪,保 head + 标记 + tail。
17
+ * 32000 ≈ 9k~19k token(视内容密度),对 256k 窗口 <8%:足以让「一次读一个完整大文件/大段代码」
18
+ * 完整进上下文(旧值 8000 只覆盖 25~30% 窗口占比 1~2%,曾迫使模型分批小读 + 引发幽灵区间事故)。
19
+ * 防爆责任后移给分层清理:微压缩存根 → 60% 低压(REFETCHABLE)→ 80% 高压 LLM 摘要。
20
+ */
21
+ export const MAX_HISTORY_RESULT = 32000;
17
22
  /** use_skill 结果(SKILL.md 正文)的放宽上限:指令须完整,中截会破坏语义。 */
18
23
  export const MAX_SKILL_RESULT = 64000;
19
24
  /** 微压缩时旧工具结果截到的存根长度(字符)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {