mocode-ai 1.7.10 → 1.7.11

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.
@@ -12,6 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { gzipSync, gunzipSync } from 'node:zlib';
15
+ import { COMPACTED_DIRNAME, isCompactedAssetName } from './compact-archive.js';
15
16
  import { TextSearchIndex } from '../context/text-search.js';
16
17
  import { config } from '../config/index.js';
17
18
  import { runReflection, snapshotTranscript } from '../memory/index.js';
@@ -174,15 +175,45 @@ export function planRetention(sessionsRoot, policy = DEFAULT_RETENTION_POLICY, n
174
175
  return { items, bytesReclaimable: items.reduce((s, i) => s + i.sizeBytes, 0) };
175
176
  }
176
177
  // ── 执行 ─────────────────────────────────────────────────────────────────
177
- /** 归档单个会话:读 session.json → gzip → 写 index → 删原目录。失败抛错由调用方统一处理。 */
178
+ /** 收集 <id>/compacted/ 下白名单资产为 relPath → utf8;无/读失败返空对象。 */
179
+ function readCompactedAssets(sessionsRoot, id) {
180
+ const dir = path.join(sessionsRoot, id, COMPACTED_DIRNAME);
181
+ const assets = {};
182
+ let names;
183
+ try {
184
+ names = readdirSync(dir, { withFileTypes: true })
185
+ .filter((e) => e.isFile())
186
+ .map((e) => e.name);
187
+ }
188
+ catch {
189
+ return assets;
190
+ }
191
+ for (const name of names) {
192
+ if (!isCompactedAssetName(name))
193
+ continue;
194
+ try {
195
+ assets[`${COMPACTED_DIRNAME}/${name}`] = readFileSync(path.join(dir, name), 'utf8');
196
+ }
197
+ catch {
198
+ // 单个资产读失败不拖垮归档。
199
+ }
200
+ }
201
+ return assets;
202
+ }
203
+ /** 归档单个会话:读 session.json(+compacted 资产)→ gzip → 写 index → 删原目录。失败抛错由调用方统一处理。 */
178
204
  function archiveSession(sessionsRoot, id, now) {
179
205
  const paths = retentionPaths(sessionsRoot);
180
206
  const dir = path.join(sessionsRoot, id);
181
207
  const sessionPath = path.join(dir, 'session.json');
182
208
  const record = JSON.parse(readFileSync(sessionPath, 'utf8'));
209
+ // 把压缩归档资产(seg/index/截断原文 blob)内嵌进 gz,避免 rmSync 目录时永久丢失
210
+ // ——否则 30 天后 resume,历史回捞链整体消失。purge 时随 gz 一起删。
211
+ const assets = readCompactedAssets(sessionsRoot, id);
212
+ if (Object.keys(assets).length > 0)
213
+ record.compactedAssets = assets;
183
214
  mkdirSync(paths.archiveDir, { recursive: true });
184
215
  const gzPath = path.join(paths.archiveDir, `${id}.json.gz`);
185
- writeFileSync(gzPath + '.tmp', gzipSync(readFileSync(sessionPath)));
216
+ writeFileSync(gzPath + '.tmp', gzipSync(Buffer.from(JSON.stringify(record), 'utf8')));
186
217
  renameSync(gzPath + '.tmp', gzPath);
187
218
  upsertArchiveIndex(paths.indexPath, {
188
219
  id: record.id,
@@ -3,8 +3,9 @@ import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
4
4
  import { gzipSync } from 'node:zlib';
5
5
  import path from 'node:path';
6
- import { MAX_TRACE_BYTES, MAX_TRACE_ROTATIONS } from '../tools/constants.js';
6
+ import { MAX_TRACE_BYTES, MAX_TRACE_ROTATIONS, MAX_COMPACTED_ARCHIVE_BYTES } from '../tools/constants.js';
7
7
  import { loadArchivedSession, readArchiveIndex, retentionPaths } from './retention.js';
8
+ import { COMPACTED_DIRNAME, isCompactedAssetName, parseSegmentMeta, renderArchiveIndex, renderCompactedSegment, resultBlobFileName, RESULT_BLOB_FILE_RE, SEGMENT_FILE_RE, } from './compact-archive.js';
8
9
  import { config, getActiveModel } from '../config/index.js';
9
10
  import { getWorkspaceRoot } from '../workspace/index.js';
10
11
  import { isToolRouteGroupName } from '../config/profiles.js';
@@ -46,6 +47,14 @@ function firstUserOf(history) {
46
47
  }
47
48
  return '';
48
49
  }
50
+ /** compacted/ 总量上限:MOCODE_MAX_COMPACTED_BYTES 覆盖(供测试/小盘),非法值回退默认。 */
51
+ function envMaxCompactedBytes() {
52
+ const raw = process.env.MOCODE_MAX_COMPACTED_BYTES;
53
+ if (!raw)
54
+ return MAX_COMPACTED_ARCHIVE_BYTES;
55
+ const n = Number(raw);
56
+ return Number.isFinite(n) && n >= 0 ? n : MAX_COMPACTED_ARCHIVE_BYTES;
57
+ }
49
58
  /**
50
59
  * Runtime-local session persistence and identity.
51
60
  *
@@ -240,6 +249,175 @@ export class SessionStore {
240
249
  }
241
250
  }
242
251
  }
252
+ /**
253
+ * 压缩归档:把本次压缩丢弃的旧区消息渲染成 `compacted/seg-NNN.md`,
254
+ * 并重建 `compacted/index.md`。best-effort——这是压缩的增值能力,不是用户数据主副本
255
+ * (session.json 仍是主副本),任何失败静默降级,绝不阻塞压缩。
256
+ *
257
+ * 编号:扫描已有 seg-NNN.md 取 max+1(外部删除/重命名后仍单调)。
258
+ * 淘汰:全部 seg 总量超 MAX_COMPACTED_ARCHIVE_BYTES 时,从最老段硬删并重建 index。
259
+ * 目录缓存失真时与 appendTrace 同模式:清缓存重试一次。
260
+ *
261
+ * @returns 段相对路径(供摘要写指针);失败返 null。
262
+ */
263
+ archiveCompactedMessages(sessionId, messages) {
264
+ if (!sessionId || messages.length === 0)
265
+ return null;
266
+ const dir = path.join(this.sessionsRoot, sessionId, COMPACTED_DIRNAME);
267
+ const budget = envMaxCompactedBytes();
268
+ for (let attempt = 0; attempt < 2; attempt++) {
269
+ try {
270
+ this.ensureDir(dir);
271
+ const seq = this.nextSegmentSeq(dir);
272
+ const { markdown, meta } = renderCompactedSegment(seq, messages);
273
+ writeFileSync(path.join(dir, meta.file), markdown, 'utf8');
274
+ this.pruneCompacted(dir, budget);
275
+ // 新增了一个 seg,index 必须重建才能列全(prune 是否删旧段不影响这一点;
276
+ // result blob 不走这里,故不需要每次写 blob 都重建)。
277
+ this.rebuildCompactedIndex(sessionId, dir);
278
+ return `.mocode/sessions/${sessionId}/${COMPACTED_DIRNAME}/${meta.file}`;
279
+ }
280
+ catch {
281
+ this.invalidateEnsuredDir(dir);
282
+ if (attempt === 1)
283
+ return null;
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ /**
289
+ * push-time 截断兜底:工具完整结果在进 history 前被 capToolResultForHistory 裁掉时,
290
+ * 把**完整原文**另存为 `compacted/result-<toolCallId>.txt`,供事后 grep/read_file 回捞。
291
+ * best-effort,写失败静默(绝不阻塞工具回灌);纳入与段相同的总量预算淘汰。
292
+ *
293
+ * @returns 全文相对路径(供 history 截断内容留指针);失败返 null。
294
+ */
295
+ archiveToolResult(sessionId, toolCallId, fullOutput) {
296
+ if (!sessionId || !toolCallId || !fullOutput)
297
+ return null;
298
+ const dir = path.join(this.sessionsRoot, sessionId, COMPACTED_DIRNAME);
299
+ const fileName = resultBlobFileName(toolCallId);
300
+ for (let attempt = 0; attempt < 2; attempt++) {
301
+ try {
302
+ this.ensureDir(dir);
303
+ writeFileSync(path.join(dir, fileName), fullOutput, 'utf8');
304
+ this.pruneCompacted(dir, envMaxCompactedBytes());
305
+ return `.mocode/sessions/${sessionId}/${COMPACTED_DIRNAME}/${fileName}`;
306
+ }
307
+ catch {
308
+ this.invalidateEnsuredDir(dir);
309
+ if (attempt === 1)
310
+ return null;
311
+ }
312
+ }
313
+ return null;
314
+ }
315
+ /**
316
+ * resume 归档会话时,把 gz 内嵌的 compacted 资产(seg/index/截断原文 blob)写回磁盘,
317
+ * 恢复历史回捞链。best-effort:任何坏条目跳过。防 zip-slip:只接受 compacted/ 下
318
+ * 白名单单层文件名。
319
+ */
320
+ restoreCompactedAssets(id, record) {
321
+ const assets = record.compactedAssets;
322
+ if (!assets || typeof assets !== 'object')
323
+ return;
324
+ const dir = path.join(this.sessionsRoot, id, COMPACTED_DIRNAME);
325
+ for (const [relPath, content] of Object.entries(assets)) {
326
+ if (typeof content !== 'string')
327
+ continue;
328
+ const base = relPath.startsWith(`${COMPACTED_DIRNAME}/`) ? relPath.slice(COMPACTED_DIRNAME.length + 1) : null;
329
+ if (!base || base.includes('/') || base.includes('\\') || !isCompactedAssetName(base))
330
+ continue;
331
+ try {
332
+ this.ensureDir(dir);
333
+ writeFileSync(path.join(dir, base), content, 'utf8');
334
+ }
335
+ catch {
336
+ this.invalidateEnsuredDir(dir);
337
+ }
338
+ }
339
+ }
340
+ /** 扫描 seg-NNN.md 取下一个编号;损坏文件名跳过。 */
341
+ nextSegmentSeq(dir) {
342
+ let max = 0;
343
+ for (const name of this.listDirNames(dir)) {
344
+ const m = SEGMENT_FILE_RE.exec(name);
345
+ if (m)
346
+ max = Math.max(max, Number(m[1]));
347
+ }
348
+ return max + 1;
349
+ }
350
+ /**
351
+ * compacted/ 全目录(seg 段 + result blob)统一字节预算淘汰:
352
+ * 超 MAX_COMPACTED_ARCHIVE_BYTES 时从最老文件(mtime,再按名)硬删。
353
+ * 唯一豁免 = 编号最大的那个 seg(始终保住最近一代压缩的完整回捞入口);
354
+ * index.md 不占删除候选(段变更后重建)。
355
+ *
356
+ * @returns 是否有 seg 被删(调用方据此重建 index)。
357
+ */
358
+ pruneCompacted(dir, budget) {
359
+ if (!(budget > 0))
360
+ return false;
361
+ const newestSeq = this.listDirNames(dir).reduce((max, name) => {
362
+ const m = SEGMENT_FILE_RE.exec(name);
363
+ return m ? Math.max(max, Number(m[1])) : max;
364
+ }, 0);
365
+ const candidates = this.listDirNames(dir)
366
+ .filter((name) => SEGMENT_FILE_RE.test(name) || RESULT_BLOB_FILE_RE.test(name))
367
+ .map((name) => {
368
+ const m = SEGMENT_FILE_RE.exec(name);
369
+ const st = statSync(path.join(dir, name));
370
+ return { name, size: st.size, mtimeMs: st.mtimeMs, seq: m ? Number(m[1]) : null };
371
+ })
372
+ // 最老在前:blob 与跨类型文件按 mtime;同刻按名,保证确定性。
373
+ .sort((a, b) => a.mtimeMs - b.mtimeMs || (a.name < b.name ? -1 : 1));
374
+ let total = candidates.reduce((s, e) => s + e.size, 0);
375
+ let removedSeg = false;
376
+ for (const e of candidates) {
377
+ if (total <= budget)
378
+ break;
379
+ if (e.seq !== null && e.seq === newestSeq)
380
+ continue; // 永不删最新段
381
+ try {
382
+ unlinkSync(path.join(dir, e.name));
383
+ }
384
+ catch {
385
+ break; // 删不动就停止淘汰,避免死循环
386
+ }
387
+ total -= e.size;
388
+ if (e.seq !== null)
389
+ removedSeg = true;
390
+ }
391
+ return removedSeg;
392
+ }
393
+ /** 仅凭现存 seg 文件重建 index.md(段被外部增删后也一致)。 */
394
+ rebuildCompactedIndex(sessionId, dir) {
395
+ const metas = [];
396
+ for (const name of this.listDirNames(dir)) {
397
+ if (!SEGMENT_FILE_RE.test(name))
398
+ continue;
399
+ try {
400
+ const meta = parseSegmentMeta(readFileSync(path.join(dir, name), 'utf8'));
401
+ if (meta)
402
+ metas.push(meta);
403
+ }
404
+ catch {
405
+ // 单个段损坏不拖垮索引。
406
+ }
407
+ }
408
+ const relativeDir = `.mocode/sessions/${sessionId}/compacted`;
409
+ writeFileSync(path.join(dir, 'index.md'), renderArchiveIndex(metas, relativeDir), 'utf8');
410
+ }
411
+ listDirNames(dir) {
412
+ try {
413
+ return readdirSync(dir, { withFileTypes: true })
414
+ .filter((e) => e.isFile())
415
+ .map((e) => e.name);
416
+ }
417
+ catch {
418
+ return [];
419
+ }
420
+ }
243
421
  /** 读取 <id>/usage.jsonl 全部记录;文件不存在或损坏行跳过。 */
244
422
  readUsage(id) {
245
423
  const file = path.join(this.sessionsRoot, id, 'usage.jsonl');
@@ -308,14 +486,20 @@ export class SessionStore {
308
486
  }
309
487
  }
310
488
  }
311
- load(id) {
489
+ load(id, opts) {
312
490
  const currentPath = this.sessionPath(id);
313
491
  const legacyPath = path.join(this.sessionsRoot, `${id}.json`);
314
492
  const source = existsSync(currentPath) ? currentPath : legacyPath;
315
493
  // 活会话与 legacy 单文件都没有:回退从归档 gz 取回(30-90 天的会话仍可 resume)。
316
494
  if (!existsSync(source)) {
317
495
  const archived = loadArchivedSession(this.sessionsRoot, id);
318
- return archived ?? null;
496
+ if (!archived)
497
+ return null;
498
+ // 仅 resume 显式要求时把 gz 内嵌的 compacted 资产写回磁盘(load 也被会话列表等
499
+ // 只读路径调用,不能在那些场景产生写盘副作用)。
500
+ if (opts?.restoreAssets)
501
+ this.restoreCompactedAssets(id, archived);
502
+ return archived;
319
503
  }
320
504
  try {
321
505
  const rec = JSON.parse(readFileSync(source, 'utf8'));
@@ -160,7 +160,8 @@ export const grepTool = {
160
160
  name: 'grep',
161
161
  description: 'Search file contents by regex (recursive; powered by ripgrep: respects .gitignore, skips binaries).\n' +
162
162
  'Output: per-file header "<path>: N matches, lines [l1, l2, ...]" plus rendered lines with ORIGINAL INDENTATION kept.\n' +
163
- 'Pass context=2..5 to include neighbouring lines inline (like ripgrep -C) instead of following each hit with a read_file round-trip. Hard wall 120s; narrow the glob if a tree contains network mounts. Use read_file when you need a whole region or exact edit text.',
163
+ 'Pass context=2..5 to include neighbouring lines inline (like ripgrep -C) instead of following each hit with a read_file round-trip. Hard wall 120s; narrow the glob if a tree contains network mounts. Use read_file when you need a whole region or exact edit text.\n' +
164
+ 'Note: .mocode/ is gitignored; to search session data (e.g. .mocode/sessions/<id>/compacted archived history) you MUST narrow glob to that path, e.g. glob=".mocode/sessions/**".',
164
165
  parameters: {
165
166
  type: 'object',
166
167
  properties: {
@@ -200,7 +201,9 @@ export const grepTool = {
200
201
  controller.abort();
201
202
  }, GREP_TIMEOUT_MS);
202
203
  try {
203
- const result = await runRipgrep({ pattern, glob, context }, controller.signal);
204
+ // glob 显式限定到 .mocode(gitignore 内)时放开 VCS ignore,否则 rg 零文件退出。
205
+ const noIgnoreVcs = glob === '.mocode/**' || glob.startsWith('.mocode/');
206
+ const result = await runRipgrep({ pattern, glob, context, noIgnoreVcs }, controller.signal);
204
207
  switch (result.status) {
205
208
  case 'regex_error':
206
209
  return `错误:非法正则 ${pattern}: ${result.message}`;
@@ -1,4 +1,5 @@
1
1
  import { addTriple, graphStats, neighborsOf, pathBetween } from '../../memory/graph.js';
2
+ import { formatRecallStats } from '../../memory/recall-stats.js';
2
3
  // ---------- memory_graph ----------
3
4
  // 知识图谱维护工具:邻居遍历(1-3 跳,可 relation 过滤)、两点最短路径、手工加三元组、看图统计。
4
5
  // 关键词搜索已并入 memory_search(条目 + 图谱事实一次返回),本工具不再提供 search。
@@ -55,6 +56,7 @@ export const memoryGraphTool = {
55
56
  `实体 ${s.entities} 条边(active ${s.edgesActive} / 已失效 ${s.edgesInvalid})`,
56
57
  `project: ${s.byScope.project.entities} 实体, ${s.byScope.project.edges} 边`,
57
58
  `global: ${s.byScope.global.entities} 实体, ${s.byScope.global.edges} 边`,
59
+ formatRecallStats(),
58
60
  ].join('\n');
59
61
  }
60
62
  if (action === 'neighbors') {
@@ -1,13 +1,14 @@
1
1
  import { searchEntries } from '../../memory/store.js';
2
- import { searchGraph } from '../../memory/graph.js';
2
+ import { searchGraphFacts } from '../../memory/graph.js';
3
+ import { recordRecallSearch } from '../../memory/recall-stats.js';
3
4
  // ---------- memory_search ----------
4
5
  // 唯一记忆搜索入口:关键词搜记忆正文(多词子串匹配,name 权重最高)+ 知识图谱事实段
5
- // (命中实体的 active 边)。命中条目即 bump recallCount(遗忘衰减依据)。
6
- // 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
6
+ // (CJK 分词加权、边级相关度 + 一跳邻接,跨 global/project 两 scope)。命中条目即 bump
7
+ // recallCount(遗忘衰减依据)。结果走 capToolResultForHistory 的放宽上限(同 use_skill)。
7
8
  const GRAPH_FACTS_LIMIT = 10;
8
9
  export const memorySearchTool = {
9
10
  name: 'memory_search',
10
- description: 'Search memory entries by keyword (substring match), returning full body. Also surfaces knowledge-graph facts (active edges) for entities matching the query.',
11
+ description: 'Search memory entries by keyword (substring match), returning full body. Also surfaces knowledge-graph facts (active edges across project + global), ranked by CJK-aware term overlap on entity names/aliases/relation/fact, plus one-hop neighbor edges for context.',
11
12
  parameters: {
12
13
  type: 'object',
13
14
  properties: {
@@ -37,23 +38,28 @@ export const memorySearchTool = {
37
38
  const entryText = r
38
39
  .map((e) => `# [${e.id}] ${e.name} (${e.type}, recalled ${e.recallCount})\nsummary: ${e.summary}\n\n${e.body}`)
39
40
  .join('\n\n---\n\n');
40
- // 知识图谱事实段:命中实体的 active 边(容错:图坏了不连累条目搜索)。
41
+ // 知识图谱事实段:边级相关度检索(容错:图坏了不连累条目搜索)。
41
42
  let graphText = '';
43
+ let graphHit = false;
42
44
  try {
43
- const g = searchGraph(query, 8);
44
- if (g.edges.length > 0) {
45
- const lines = g.edges
46
- .slice(0, GRAPH_FACTS_LIMIT)
47
- .map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`);
48
- const more = g.edges.length > GRAPH_FACTS_LIMIT
49
- ? `\n…(共 ${g.edges.length} 条,其余用 memory_graph action=neighbors 展开)`
50
- : '';
51
- graphText = `\n\n## 知识图谱事实\n${lines.join('\n')}${more}`;
45
+ const hits = searchGraphFacts(query, { scope: 'all', limit: GRAPH_FACTS_LIMIT, neighbors: 4 });
46
+ graphHit = hits.length > 0;
47
+ if (graphHit) {
48
+ const lines = hits.map((h) => {
49
+ const src = h.srcEntity?.name ?? h.edge.src;
50
+ const dst = h.dstEntity?.name ?? h.edge.dst;
51
+ const fact = h.edge.fact ? ` (${h.edge.fact})` : '';
52
+ const tag = h.viaNeighbor ? ' [neighbor]' : '';
53
+ const scopeTag = h.edge.scope === 'global' ? '[global]' : '[project]';
54
+ return `${scopeTag} ${src} --[${h.edge.relation}]--> ${dst}${fact}${tag}`;
55
+ });
56
+ graphText = `\n\n## 知识图谱事实\n${lines.join('\n')}`;
52
57
  }
53
58
  }
54
59
  catch {
55
60
  // 静默:图谱段是增强,失败只降级为纯条目结果
56
61
  }
62
+ recordRecallSearch(graphHit); // 埋点:pull 召回使用情况(写盘失败静默)
57
63
  if (!entryText && !graphText)
58
64
  return `(无匹配记忆:query="${query}")`;
59
65
  if (!entryText)
@@ -29,6 +29,10 @@ export const MAX_OLD_TOOL_STUB = 600;
29
29
  export const MAX_TRACE_BYTES = 512 * 1024;
30
30
  /** 保留的历史 trace 轮转份数(trace.1 .. trace.N),更老的硬删。 */
31
31
  export const MAX_TRACE_ROTATIONS = 3;
32
+ /** compacted/ 压缩归档目录总量上限(字节):旧区原文逐段累积,超了从最老 seg 淘汰。
33
+ * 段大小与窗口正相关(200k 窗口单段约几百 KB),10MiB 约覆盖十几~几十次压缩。
34
+ * 可用 MOCODE_MAX_COMPACTED_BYTES 覆盖(测试/小盘用)。 */
35
+ export const MAX_COMPACTED_ARCHIVE_BYTES = 10 * 1024 * 1024;
32
36
  /** rollback snapshots 压缩前滚动窗口:无论是否 compact,仅保留最近 N 个 user turn 的快照。 */
33
37
  export const MAX_SNAPSHOT_TURNS = 50;
34
38
  // ── 上下文压缩摘要(见 session/compact.ts defaultSummarize)────────────────
@@ -69,6 +69,9 @@ export function runRipgrep(params, signal) {
69
69
  // --no-require-git:rg 默认只在 git 仓库内才读 .gitignore;加上后非 git 项目的 .gitignore
70
70
  // 同样生效(实测不加时 ignored/ 会被扫到)。
71
71
  const args = ['--json', '--hidden', '--no-require-git', '--path-separator', '/'];
72
+ // glob 显式指向 gitignore 内的 .mocode(回捞压缩归档等)时放开 VCS ignore。
73
+ if (params.noIgnoreVcs)
74
+ args.push('--no-ignore-vcs');
72
75
  if (params.context > 0)
73
76
  args.push('-C', String(params.context));
74
77
  if (params.glob && params.glob !== '**/*')
@@ -169,7 +172,14 @@ export function runRipgrep(params, signal) {
169
172
  handleLine(buffer.trim());
170
173
  // exit 2 + stderr = 正则/参数错误;无命中事件时按 regex_error 上报。
171
174
  if (code === 2 && byPath.size === 0 && stderr.trim()) {
172
- finish({ status: 'regex_error', message: stderr.trim() });
175
+ const message = stderr.trim();
176
+ // 过滤器把所有文件排除(典型:glob 指向 .gitignore 内的 .mocode 却没放开 ignore)
177
+ // 不是正则错误——按无匹配处理,避免误导模型去改 pattern。
178
+ if (/No files were searched/i.test(message)) {
179
+ finish({ status: 'no-match', scanned, truncated });
180
+ return;
181
+ }
182
+ finish({ status: 'regex_error', message });
173
183
  return;
174
184
  }
175
185
  if (byPath.size === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.7.10",
3
+ "version": "1.7.11",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {