mocode-ai 1.6.9 → 1.6.10

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.
@@ -52,8 +52,12 @@ export const CAPABILITIES = {
52
52
  // 后台进程与浏览器会话跨调用存活;串行执行避免同一页面/服务被并发操作。
53
53
  dev_server: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
54
54
  browser: { effect: 'process', concurrency: 'serial', resources: workspaceResource },
55
- glob: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
56
- grep: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
55
+ // glob/grep 不取任何资源锁:读枚举容忍命令执行期间的瞬时不一致(一次目录扫描的结果
56
+ // 本来就无事务语义),换取「一条 run_command/dev_server 持 workspace 写锁期间,只读
57
+ // 枚举与并行子 agent 的读不被全面压制」。文件级一致性仍由 read_file 的 file:<path>
58
+ // 锁保证(与 file-locked 写互斥)。网络只读同理不锁。
59
+ glob: { effect: 'read', concurrency: 'parallel', resources: () => [], supportsAbort: true },
60
+ grep: { effect: 'read', concurrency: 'parallel', resources: () => [], supportsAbort: true },
57
61
  // 网络只读查询:无副作用,可安全重复执行 → idempotent 让 runtime 对瞬时失败(429/5xx/超时/
58
62
  // 网络抖动)自动退避重试,省掉模型「再发一轮 tool call 自救」的往返。写/进程类工具绝不置此位。
59
63
  web_search: { effect: 'network', concurrency: 'parallel', supportsAbort: true, idempotent: true },
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import { MAX_OUTPUT } from '../constants.js';
3
3
  import { getSandboxRoot, filterEnv, isCommandDenied, jailResolve } from '../../sandbox/index.js';
4
4
  import { t } from '../../i18n/index.js';
@@ -77,12 +77,22 @@ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_M
77
77
  });
78
78
  const output = new BoundedCommandOutput();
79
79
  let finished = false;
80
- const killTree = () => {
80
+ // 异步树杀:spawnSync('taskkill') 会同步阻塞整个事件循环(扫进程树期间流式渲染、
81
+ // 其它并行工具全部停摆)。taskkill 自身是独立进程,发出去后等它退出即可;
82
+ // 只在 abort/timeout 路径调用,正常退出(close)不杀。
83
+ const killTree = async () => {
81
84
  try {
82
85
  if (isWin) {
83
- if (child.pid != null) {
84
- spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
85
- }
86
+ if (child.pid == null)
87
+ return;
88
+ await new Promise((resolve) => {
89
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
90
+ stdio: 'ignore',
91
+ windowsHide: true,
92
+ });
93
+ killer.on('close', () => resolve());
94
+ killer.on('error', () => resolve());
95
+ });
86
96
  }
87
97
  else {
88
98
  child.kill('SIGTERM');
@@ -100,23 +110,35 @@ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_M
100
110
  signal?.removeEventListener('abort', onAbort);
101
111
  done({ ...result, durationMs: Date.now() - startedAt });
102
112
  };
113
+ // 树杀(异步)进行中:child 的 close 事件可能由 taskkill 触发并抢在 finish 前,
114
+ // 必须忽略之,否则 abort/timeout 会被误报成 failed(进程被杀的 exitCode ≠ 0)。
115
+ let pendingTermination = null;
116
+ const terminate = (status) => {
117
+ if (finished || pendingTermination)
118
+ return;
119
+ pendingTermination = status;
120
+ void killTree().then(() => {
121
+ finish({ status, exitCode: null, output: output.render().trim() });
122
+ });
123
+ };
103
124
  const onAbort = () => {
104
- killTree();
105
- finish({ status: 'aborted', exitCode: null, output: output.render().trim() });
125
+ terminate('aborted');
106
126
  };
107
127
  const onChunk = (chunk) => output.append(chunk.toString('utf8'));
108
128
  // 先起 timer 再挂事件:finish/onAbort 是闭包,里面要 clearTimeout(timer)。
109
129
  // 把 timer 的初始化提到所有引用它的注册点之前,避免依赖"事件回调必然异步"这一前提。
110
130
  const timer = setTimeout(() => {
111
- killTree();
112
- finish({ status: 'timed_out', exitCode: null, output: output.render().trim() });
131
+ terminate('timed_out');
113
132
  }, effectiveTimeout);
114
133
  child.stdout.on('data', onChunk);
115
134
  child.stderr.on('data', onChunk);
116
135
  child.on('error', (error) => {
117
- finish({ status: 'spawn_error', exitCode: null, output: error.message });
136
+ if (!pendingTermination)
137
+ finish({ status: 'spawn_error', exitCode: null, output: error.message });
118
138
  });
119
139
  child.on('close', (code) => {
140
+ if (pendingTermination)
141
+ return;
120
142
  finish({
121
143
  status: code === 0 ? 'passed' : 'failed',
122
144
  exitCode: code,
@@ -56,7 +56,7 @@ function addToolGroupsSchema(groups) {
56
56
  type: 'function',
57
57
  function: {
58
58
  name: ADD_TOOL_GROUPS_TOOL_NAME,
59
- description: 'Expand the tool surface for this turn when the current tools are insufficient. Call this tool ALONE, before any dependent tool call. Added groups become available on the next model step and cannot be removed during the turn.',
59
+ description: 'Expand the tool surface for this turn when the current tools are insufficient. Call it alone or batched with read-only tools (read_file/glob/grep/web_search/web_fetch): the read-only calls run in the same step and the new groups take effect on the next model step. Write/execute tools batched with it are skipped and must be retried on the next step, so keep calls that depend on the newly added groups for the next step. Groups cannot be removed during the turn.',
60
60
  parameters: {
61
61
  type: 'object',
62
62
  properties: {
@@ -237,7 +237,7 @@ export class ToolPolicyController {
237
237
  '## Tool route (current turn)',
238
238
  `Policy ${snapshot.id} v${snapshot.version}; active groups: ${active}.`,
239
239
  `Router reason: ${snapshot.reason}`,
240
- 'Use only the exposed tools. Missing capability? Call add_tool_groups — alone, or batched with read-only tools (read_file/glob/grep/web_search/web_fetch); the read-only calls run in that same step and the new groups take effect on the next step. Do not batch it with write/execute tools, and keep calls that depend on the newly added groups in the next step.',
240
+ 'Use only the exposed tools. Missing capability? Call add_tool_groups — alone, or batched with read-only tools (read_file/glob/grep/web_search/web_fetch); the read-only calls run in that same step and the new groups take effect on the next step. Write/execute tools batched with it are skipped (retry them on the next step), and calls that depend on the newly added groups belong in the next step.',
241
241
  ];
242
242
  if (remaining.length)
243
243
  lines.push(`Groups still available: ${remaining.join(', ')}.`);
@@ -122,7 +122,11 @@ export function resolveResourceLockRequests(capabilities, args) {
122
122
  return workspaceWrite();
123
123
  }
124
124
  if (keys.length === 0) {
125
- return capabilities.effect === 'network' ? [] : workspaceWrite();
125
+ // 显式声明「无资源」(glob/grep 等读枚举、网络只读)即不锁:
126
+ // 读枚举容忍命令执行期间的瞬时不一致,换取 workspace 写锁期间的读不被压制。
127
+ if (capabilities.effect === 'network' || capabilities.effect === 'read')
128
+ return [];
129
+ return workspaceWrite();
126
130
  }
127
131
  const mode = modeFor(capabilities.effect);
128
132
  const requests = [];
@@ -32,10 +32,18 @@ export function resolveRgPath() {
32
32
  const probe = spawnSync(isWin ? 'rg.exe' : 'rg', ['--version'], { stdio: 'ignore', windowsHide: true });
33
33
  return probe.status === 0 ? (isWin ? 'rg.exe' : 'rg') : null;
34
34
  }
35
+ // 异步树杀:spawnSync('taskkill') 会同步阻塞事件循环(扫树期间流式渲染/并行工具停摆)。
36
+ // 两处调用都是 fire-and-forget(结果由 close 事件收口,finish 有 settled 守卫)。
35
37
  function killTree(child) {
36
38
  try {
37
39
  if (isWin && child.pid != null) {
38
- spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
40
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
41
+ stdio: 'ignore',
42
+ windowsHide: true,
43
+ });
44
+ killer.on('error', () => {
45
+ // 进程已退出或终止失败,尽力而为。
46
+ });
39
47
  }
40
48
  else {
41
49
  child.kill('SIGTERM');
package/dist/ui/batch.js CHANGED
@@ -519,10 +519,11 @@ const CARET_NONE = ' ';
519
519
  * 曾为 4 空格——摘要行(`◆ 探索 …`)与工具调用行之间留白过大,工具名被推得过右(用户实测反馈);
520
520
  * 子批再叠 8 空格后层级被拉散。收成 2 空格后每层固定递进 2 列,层级仍清晰但不空旷。 */
521
521
  const ENTRY_INDENT = ' ';
522
- /** 子 agent 批摘要行的缩进(= 两层 entry 缩进)。
523
- * 子批摘要挂在父批的 entry 行之下,是第二层;硬编码会与 ENTRY_INDENT 脱钩,
524
- * 故由 ENTRY_INDENT 推得。 */
525
- export const SUB_BATCH_INDENT = ENTRY_INDENT + ENTRY_INDENT;
522
+ /** 子 agent 批摘要行的缩进(= 三层 entry 缩进)。
523
+ * 子批摘要挂在父批的 entry 行之下,entry 行本身已带 2 空格;此前取两层(4)时子批
524
+ * 摘要与父 entry 行仅差 2 列,视觉上挤在同一层级(用户实测反馈)。取三层(6)后,
525
+ * 子批摘要比父 entry 行深 4 列,父子层级一眼可分;仍由 ENTRY_INDENT 推导,不脱钩。 */
526
+ export const SUB_BATCH_INDENT = ENTRY_INDENT + ENTRY_INDENT + ENTRY_INDENT;
526
527
  /** 详情行缩进:5 空格(对齐 entry 行的 " ▸ " 之后再右移一列,形成层次)。 */
527
528
  const DETAIL_INDENT = ' ';
528
529
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.6.9",
3
+ "version": "1.6.10",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {