mocode-ai 1.6.6 → 1.6.7

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
@@ -39,7 +39,7 @@ See mocode complete real tasks autonomously:
39
39
  MoCode keeps code-level control light and leaves task strategy to the agent:
40
40
 
41
41
  - **Advisory working discipline** — The system prompt asks the agent to make focused changes, avoid redundant retrieval, and decide for itself whether validation is useful. Validation is optional and is never a completion gate.
42
- - **Transparent tool failures** — Each tool call runs once and returns its raw structured failure to the agent, which decides whether and how to recover.
42
+ - **Bounded tool retries, explicit failures** — The runtime automatically retries only transient errors from tools explicitly marked `idempotent`, at most twice. `TIMEOUT`, permission denial, cancellation, and non-idempotent calls are not automatically retried. The resulting structured outcome is returned to the agent for any further recovery decision; see [Automatic retry](#automatic-retry-the-retryable-contract).
43
43
  - **`ask_human` for user-owned decisions** — The agent asks only when repository evidence cannot resolve a high-impact choice; implementation details remain autonomous.
44
44
  - **Five-zone context controls + token self-calibration** — Five independent dials (`autoCompact` / `contextOptimize` / `contextRelprune` / `contextLifecycle` / `contextBudget`) manage context pressure. Token estimation self-calibrates against provider usage.
45
45
 
@@ -47,29 +47,29 @@ MoCode keeps code-level control light and leaves task strategy to the agent:
47
47
 
48
48
  MoCode is organized as a layered runtime: the terminal experience drives an autonomous core, the core reaches capabilities through a guarded execution plane, and a persistent intelligence layer keeps long-running work coherent.
49
49
 
50
- <p align="center"><img src="./assets/architecture/system-overview.svg" alt="MoCode layered system architecture" width="100%"></p>
50
+ Implementation entry points: [Agent core](src/agent/core.ts), [tool dispatch](src/agent/stages/tool-dispatcher.ts), and [tool execution](src/tools/tool-runtime.ts).
51
51
 
52
52
  ### Autonomous execution loop
53
53
 
54
54
  Each model response is one step in a closed loop. Tool calls are classified by declared capabilities, safe reads can run in parallel, and writes acquire canonical resource locks. Tool evidence returns to history unchanged apart from a hard per-result safety cap. When the agent has no more tools to call, its response completes immediately; the framework does not run hidden validation or force another model turn.
55
55
 
56
- <p align="center"><img src="./assets/architecture/agent-loop.svg" alt="MoCode autonomous agent execution loop" width="100%"></p>
57
-
58
56
  ### Context compression only under real pressure
59
57
 
60
58
  Normal sessions retain full tool evidence and structured freshness/provenance metadata. At 80% of the model window, one scheduler event runs enabled exact-supersession, stale-artifact, and old-log/search cleanup, then always compacts history. Lifecycle tracking never ages content by tool-call count.
61
59
 
62
- <p align="center"><img src="./assets/architecture/context-engine.svg" alt="MoCode context engineering and durable memory architecture" width="100%"></p>
60
+ The implementation is defined by [budget accounting](src/context/budget.ts) and [pressure scheduling](src/session/scheduler.ts).
61
+
62
+ ### Multi-agent work in a shared workspace
63
63
 
64
- ### Multi-agent work without unsafe shared writes
64
+ Sub-agents keep independent history branches, can reuse the parent's conversation prefix, and cannot expand beyond the tool set delegated by the parent step. Consecutive sub-agent calls run in bounded parallel batches (`SUB_AGENT_CONCURRENCY`, default `5`; set `1` for sequential dispatch).
65
65
 
66
- Read-only sub-agents fan out concurrently. Writer agents work inside private filesystem overlays and return structured ChangeSets; the coordinator checks expected hashes, acquires canonical locks, and performs conflict-safe merges. Validation remains an explicit agent choice in the shared workspace.
66
+ **History isolation is not filesystem isolation.** Sub-agents write directly to the shared workspace and inherit the parent's current rollback turn. There is no private filesystem overlay, per-worker worktree, or post-task ChangeSet merge. Nested tools acquire their own resource locks; supported file edits enforce their own expected-hash checks. These guards do not make an entire multi-step sub-task an isolated transaction. The parent receives the final summary, status, usage, and tracked changed files rather than the full worker transcript. Validation remains an explicit agent choice.
67
67
 
68
- <p align="center"><img src="./assets/architecture/multi-agent.svg" alt="MoCode multi-agent overlay and ChangeSet coordination" width="100%"></p>
68
+ See [sub-agent execution](src/agent/spawn.ts), [the sub-agent tool](src/tools/builtins/task.ts), and [capability-based dispatch](src/agent/stages/tool-dispatcher.ts).
69
69
 
70
70
  ### Controlled execution: permission gates and capability scheduling
71
71
 
72
- Every mutating tool calls into a permission layer before it runs. Tools are classified `safe` / `confirm` / `dangerous`, scopes can be `once` / `session` / `project` / global-tool, fingerprints are stable hashes (command, path, or args), and the persistent record lives in `~/.mocode/permissions.json` (v3 schema, with v2 resource grants still loaded). Piped or CI environments default to deny until you opt in.
72
+ Every mutating tool calls into a permission layer before it runs. Tools are classified `safe` / `confirm` / `dangerous`, scopes can be `once` / `session` / `project` / global-tool, fingerprints are stable hashes (command, path, or args), and the persistent record lives in `~/.mocode/permissions.json` (v3 schema, with v2 resource grants still loaded). In piped or CI environments, unapproved `confirm` / `dangerous` actions are denied by default; safe operations do not require an interactive prompt.
73
73
 
74
74
  <p align="center"><img src="./assets/architecture/permission-model.svg" alt="MoCode permission model: tool classes, four-tier grants, fingerprinting, durable storage" width="100%"></p>
75
75
 
@@ -77,18 +77,16 @@ Every mutating tool calls into a permission layer before it runs. Tools are clas
77
77
 
78
78
  MoCode does not run a hidden validation cascade when a task ends. The agent can explicitly call `run_command` for a focused test, typecheck, or build when it judges that evidence useful; otherwise it may finish without an extra framework-controlled round trip.
79
79
 
80
- ### Rollback timeline: per-mutation snapshots, restore by turn
80
+ ### Rollback timeline: tracked file changes, restore by turn
81
81
 
82
- A clean undo point is saved before every mutating tool. `/rollback <turnId>` restores file buffers in reverse-chronological order under canonical resource locks — it does not re-run the model or launch automatic tests. Read tools, network effects, and binary changes are explicitly out of scope, kept honest in the contract.
82
+ Rollback records supported text-file mutations, including tracked changes made by sub-agents in the parent's turn. `/rollback` restores recorded file contents under canonical resource locks; it does not re-run the model or launch automatic tests. It is not a universal undo mechanism: network effects, desktop actions, binary changes, and other untracked effects are outside its scope. Cancelling a turn does not automatically undo completed file writes.
83
83
 
84
- <p align="center"><img src="./assets/architecture/rollback-flow.svg" alt="MoCode rollback timeline and per-turn snapshot flow" width="100%"></p>
84
+ See the [rollback store](src/rollback/store.ts) for capture and restore boundaries.
85
85
 
86
86
  ### Context controls: one pressure gate, independently optional stages
87
87
 
88
88
  The controls remain independently configurable, but automatic rewriting has exactly one trigger: corrected or raw request occupancy reaching 80%. That event runs every enabled pressure cleanup and then always compacts history. `contextLifecycle` only tracks provenance metadata, while EWMA calibration keeps the estimate aligned with provider usage.
89
89
 
90
- <p align="center"><img src="./assets/architecture/context-controls.svg" alt="MoCode context controls: five independent toggles, observation lifecycle, token self-calibration" width="100%"></p>
91
-
92
90
  ### Desktop pet: a passive mirror over WebSocket
93
91
 
94
92
  The optional Electron sub-package (`packages/pet-app`) shows a stateful floating character that mirrors agent activity via a one-way WebSocket stream. Quit with `/pet quit`. The renderer owns no business logic; the agent loop is unchanged regardless of whether the pet is running.
@@ -100,16 +98,16 @@ The optional Electron sub-package (`packages/pet-app`) shows a stateful floating
100
98
  MoCode isn't a chat box with a coat of paint — it's an agent that actually gets things done:
101
99
 
102
100
  - **Autonomous multi-step execution** — In a single conversation, the agent chains multiple steps on its own: read code, edit code, run tests, fix based on errors, and so on. It decides the next step without you nagging it. When it hits a decision point, it calls `ask_human` to pop up a panel and ask you (blocking until you respond).
103
- - **Parallel read-only tools** — Consecutive read-only operations in a turn (reading files, grep, glob, codegraph, web search/fetch) run concurrently, so total time is roughly the slowest single call instead of the sum of all of them. Operations with side effects (writing/editing files) stay sequential to preserve snapshot ordering and data safety.
104
- - **Sub-agents divide and conquer** — Complex tasks can spawn independent sub-agents with isolated histories and scoped toolsets. Read-only workers can fan out concurrently; writer workers run in private filesystem overlays and return ChangeSets that are merged under expected-hash checks and canonical resource locks. Only structured findings return to the main thread.
105
- - **Plan / Auto dual mode** — In `plan` mode the agent is read-only (reads code, queries indexes, searches — never writes to disk, runs commands, or spawns sub-agents) and produces a plan; `auto` mode permits execution. Tool capabilities are not a static “full” mode: a lightweight LLM router selects the minimum sufficient groups for each real user turn, and the main model may add groups on a later step when needed.
101
+ - **Capability-aware concurrency** — Consecutive tools declared safe to parallelize (such as file reads, grep, glob, and web reads) can run concurrently. Resource-locked file mutations can also be dispatched together: canonical locks coordinate conflicting resources rather than serializing every write globally. Process tools and tools without parallel capabilities retain their declared serial behavior.
102
+ - **Sub-agents divide and conquer** — Workers have independent history branches and inherit the parent step's tool ceiling. They run in bounded parallel batches, write directly to the shared workspace, and share the parent's rollback turn. A final summary and execution metadata return to the parent; there is no per-worker filesystem isolation or merge stage.
103
+ - **Plan / Auto dual mode** — In `plan` mode the agent explores and produces a plan without editing project files, running shell commands, or spawning sub-agents; internal session/plan records may still be persisted. `auto` mode permits execution. Tool capabilities are not a static “full” mode: a lightweight LLM router selects the minimum sufficient groups for each real user turn, and the main model may add groups on a later step when needed.
106
104
  - **Pressure-driven context compression** — Normal history keeps full tool evidence. At 80% occupancy, one scheduler event runs all enabled cleanup and always follows with a history summary. `/context` shows live usage and `/compact` remains an explicit manual override.
107
- - **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. A background process periodically reflects on conversations to mine things worth remembering. Memories can be created, searched, updated, and forgotten, with recall-based decay.
105
+ - **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. Optional background reflection mines conversations for useful memories when `AUTO_REFLECT=true` (off by default). Memories can be created, searched, updated, and forgotten, with recall-based decay.
108
106
  - **Project context (`AGENTS.md`)** — A single project-level memory file at `AGENTS.md` captures both static facts (project description, commands, module list, directory tree) and human/AI-written insights (conventions, architectural decisions, pitfalls). Generate it once with `/init`, then keep it up to date by hand or by asking the agent to refresh it. Auto-injected into the system prompt every turn, but lean by design: the `directory tree` and `extension points` sections stay out of the prompt as one-line pointers (read_file `AGENTS.md` on demand), keeping the always-on payload small. During work, the agent may append stable, non-obvious facts it discovers to `.mocode/agents-draft.md`; `/init` merges and clears that draft.
109
107
  - **Session notepad (notes.md)** — For complex multi-step tasks (≥3 file changes / ≥5 tool calls), the agent maintains a working notepad at `.mocode/sessions/<sessionId>/notes.md` (file-based, survives context compression). It records the execution plan with the dedicated `plan_update` tool — a three-state step machine (`pending`/`in_progress`/`completed`, at most one `in_progress`) that auto-settles to `## Done:` when finished. The active plan is re-injected into the system prompt after compaction and re-synced into context whenever notes.md changes, and a gentle reminder nudges the agent if it goes several tool-steps without updating the plan. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]`.
110
- - **Interruptible and reversible** — Ctrl+C interrupts the current turn at any time (kills child processes recursively, rolls history back to before the turn started, leaves no half-finished tool calls). `/rollback` restores file changes from per-turn snapshots, with a per-file keep/undo choice — no git dependency required.
108
+ - **Interruptible and reversible** — Ctrl+C propagates cancellation to model requests and supported running tools. History returns to the latest committed tool-batch checkpoint rather than unconditionally discarding the entire turn. Completed file writes remain until explicitly reverted; `/rollback` restores tracked file changes from turn snapshots with a per-file keep/undo choice, without requiring git.
111
109
  - **Input safety net** — Long prompts no longer fear a stray Enter: `Ctrl+G` opens an in-TUI composer popup (notepad-style editing — Enter inserts a newline, with soft wrap, selection, copy/cut/paste and undo; Ctrl+S fills the text back into the input box without sending). `Ctrl+R`/`Ctrl+P` fuzzy-search your input history (Enter only fills it back), and the post-send recall window widens to 2 seconds with any-key recall for long inputs.
112
- - **Sandbox protection** — File reads/writes go through a sandbox that blocks out-of-bounds paths (`../../`, absolute paths outside the root, symlink escapes, etc.), so the agent never touches files outside your working directory.
110
+ - **File-path boundaries** — Built-in file reads/writes reject paths outside `SANDBOX_ROOT` (the working directory by default), including traversal and symlink escapes. This is a file-tool path guard, not an OS-level sandbox for shell commands, MCP servers, or desktop input. Memory and skills may intentionally use configured locations outside the project. Review permissions before allowing high-risk tools; see [sandbox policy](src/sandbox/policy.ts).
113
111
  - **Computer Use (high-risk, routed only for explicit GUI intent)** — When the request genuinely requires real mouse/keyboard interaction, the router can expose the `computer-control` group and feed each resulting screenshot back to the model. `/cu off` (or `MOCODE_COMPUTER_USE_ENABLED=false`) is a hard veto; `/cu on` merely allows routing and does not keep the tool permanently visible. The blast radius exceeds file tools because OS input bypasses the file sandbox. **Use a VM / sandbox / dedicated test machine**, not a daily driver. Every action still passes the permission gate, and plan mode always blocks it. Windows first; macOS/Linux pending.
114
112
 
115
113
  ## Features
@@ -142,7 +140,7 @@ npm install -g mocode-ai
142
140
 
143
141
  This gives you the `mocode` command. Prefer not to install globally? Run it directly with `npx mocode-ai`.
144
142
 
145
- > MoCode does not contact the registry on startup. Use `/upgrade check`, `/upgrade status`, or `/upgrade now` explicitly when you want to check or install an update. Real installation is disabled in source/tsx development mode.
143
+ > MoCode does not contact the registry on startup. Use `/upgrade check`, `/upgrade status`, or `/upgrade now` explicitly when you want to check or install an update. Real installation is disabled in source/tsx development mode; see the [upgrade implementation](src/commands/upgrade.ts).
146
144
 
147
145
  ### Run from source (development / contributing)
148
146
 
@@ -153,7 +151,7 @@ npm install
153
151
  npm start
154
152
  ```
155
153
 
156
- Source runs directly via tsx, no build step. After changing code, restart `npm start` for changes to take effect (tsx loads modules at startup, no hot reload). Runtime dependencies: `openai`, `dotenv`, `fast-glob`; dev dependencies: `tsx`, `typescript`, `@types/node`.
154
+ Source runs directly via tsx, no build step. After changing code, restart `npm start` for changes to take effect (tsx loads modules at startup, no hot reload). Install from the repository root so npm resolves the workspaces. Dependency lists and development commands are maintained in [package.json](package.json), rather than duplicated here.
157
155
 
158
156
  ### Repository stacks and contributing
159
157
 
@@ -167,6 +165,10 @@ The production path is the TypeScript CLI. `packages/work-app` is incubating, `p
167
165
 
168
166
  Do not import another package's `src/` or internal `dist/` layout. Applications consume package exports and the public `mocode-agent-host` bin contract.
169
167
 
168
+ ### Keeping documentation aligned
169
+
170
+ When changing execution, permissions, concurrency, rollback, or upgrade behavior, update both `README.md` and `README.zh-CN.md` against the implementation links in these sections. Diagrams must describe the same current behavior before being embedded. Use the package manifests and built-in registry for dependency/tool inventories, and the stack-status page for release boundaries; do not treat a design proposal or an old diagram as a shipped guarantee.
171
+
170
172
  ## Configuration
171
173
 
172
174
  On first use, run the setup wizard to fill in three fields interactively (API base URL / key / model name), written to `~/.mocode/config` (global, works from any directory or terminal):
@@ -221,6 +223,8 @@ Common backend `base_url` values:
221
223
  | `MOCODE_LIFECYCLE` | Provenance metadata tracking; never ages or rewrites content | `true` |
222
224
  | `MAX_STEPS` | Max agent loop steps per turn (infinite-loop safety only) | `1000` |
223
225
  | `SUB_AGENT_MAX_STEPS` | Sub-agent loop safety ceiling; defaults to the main-agent value | `1000` |
226
+ | `SUB_AGENT_CONCURRENCY` | Concurrent sub-agent calls per dispatch batch; `1` dispatches them sequentially | `5` |
227
+ | `SUB_AGENT_MAX_DEPTH` | Maximum recursive delegation depth | `3` |
224
228
  | `SANDBOX_ROOT` | Sandbox root directory (file operation boundary; falls back to cwd if unset) | none |
225
229
  | `MOCODE_SUBAGENT_ENABLED` | Set `false` to veto the `orchestration` route group; unset/`true` allows on-demand routing | unset |
226
230
  | `MOCODE_FRONTEND_TOOLS_ENABLED` | Set `false` to veto `browser-debug` and `desktop-observe` (does not affect `background-exec`); unset/`true` allows routing | unset |
@@ -239,7 +243,7 @@ mocode --resume <id> # resume a specific session
239
243
  mocode config # edit configuration
240
244
  ```
241
245
 
242
- Running from source uses `npm start` (equivalent to `mocode`, but skips the self-update check).
246
+ Running from source uses `npm start`. Neither source nor installed runs automatically check for updates at startup; `/upgrade` is an explicit user action, and real installation is disabled in source/tsx mode.
243
247
 
244
248
  Once in the REPL, just start chatting. It launches straight into the full-screen TUI, showing a banner (model / backend / working directory / tool list). Responses stream in, with the reasoning section visible in real time before collapsing.
245
249
 
@@ -247,7 +251,9 @@ The agent operates in **the working directory it was launched from** — to have
247
251
 
248
252
  ## Tools
249
253
 
250
- Every real user turn first goes through a constrained LLM router. Nine common tools are always available (`read_file`, `glob`, `grep`, `web_search`, `web_fetch`, `plan_update`, `note_append`, `ask_human`, `use_skill`); additional capabilities are selected as composable groups for writing, shell debugging, browser debugging, desktop observation/control, memory, orchestration, and MCP. If the initial set is insufficient, the main model must call `add_tool_groups` alone; the expanded schemas appear on the next model step. A routing failure reuses the previous turn’s groups (or common-only), never the full toolset.
254
+ Every real user turn first goes through a constrained LLM router. Common tools include `read_file`, `glob`, `grep`, `web_search`, `web_fetch`, `plan_update`, `note_append`, `ask_human`, and `use_skill`; additional capabilities are selected as composable groups for writing, shell debugging, browser debugging, desktop observation/control, memory, orchestration, and MCP. If the initial set is insufficient, the main model requests more groups with `add_tool_groups`; the expanded schemas appear on the next model step. A routing failure reuses the previous turn's groups (or common-only), never the full toolset.
255
+
256
+ The tables below are selected capabilities, not an exhaustive tool inventory. Current registrations and capability declarations are maintained in the [built-in registry](src/tools/builtins/index.ts); exposure is controlled by [ToolPolicy](src/tools/policy.ts). Codegraph queries use an installed skill/CLI when available, not a standalone built-in `codegraph` tool.
251
257
 
252
258
  | Tool | Purpose |
253
259
  | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -256,24 +262,26 @@ Every real user turn first goes through a constrained LLM router. Nine common to
256
262
  | `edit_file` | Precise string replacement (`old_string` must match uniquely) |
257
263
  | `run_command` | Run a foreground shell command, merging stdout+stderr, 120s default timeout; `shell=cmd\|powershell\|bash` picks the interpreter |
258
264
  | `glob` | Find files by glob pattern (excludes node_modules/.git) |
259
- | `grep` | Regex content search, pure JS implementation, no `rg` dependency; `context=N` returns neighbouring lines inline so a hit rarely needs a follow-up read |
260
- | `codegraph` | With a `.codegraph/` index built, query symbol source and call chains (more accurate and cheaper than read_file/grep) |
265
+ | `grep` | Regex content search powered by ripgrep (bundled via `@vscode/ripgrep`; respects `.gitignore`, skips binaries), falling back to a pure-JS scan only when no `rg` binary is available; `context=N` returns neighbouring lines inline so a hit rarely needs a follow-up read |
261
266
  | `web_search` | Web search (AnySearch), returns title/URL/snippet/body |
262
267
  | `web_fetch` | Fetch a URL, cleaning HTML into plain text; browser-like headers, auto-retry on transient failures, optional plaintext-proxy fallback |
263
268
  | `use_skill` | Load the full SKILL.md instructions for a given skill |
264
269
  | `ask_human` | Pop up a Q&A panel at decision points; user picks a preset or types freely (blocks until answered) |
265
270
  | `plan_update` | Record/update the session execution plan (the `## Plan:` block in notes.md); three-state steps, at most one in_progress, auto-settles to `## Done:` when all complete |
266
- | `sub-agent` | Spawn a capable isolated worker; read tasks can run concurrently and writes use overlay + ChangeSet safe merge |
271
+ | `sub-agent` | Spawn a worker with an independent history and the parent step's tool ceiling; bounded concurrency, shared workspace and rollback turn |
267
272
 
273
+ | Memory tool | Purpose |
274
+ | ----------- | ------- |
268
275
  | `memory_save` | Save a piece of cross-session long-term memory (title indexed, body fetched on demand) |
269
276
  | `memory_search` | Search memory bodies by keyword; hits boost the recall count (affects forgetting decay) |
270
277
  | `memory_list` | List the memory index (id/title/summary, no body) |
271
278
  | `memory_update` | Edit a memory in place (id unchanged; correct stale facts / update summary / toggle pin) |
272
279
  | `memory_forget` | Forget a memory: archived by default (recoverable), `mode=delete` for a hard delete (pinned memories can't be deleted) |
280
+ | `memory_graph` | Traverse neighbors, find paths, add triples, or inspect graph statistics; keyword search is provided by `memory_search` |
273
281
 
274
282
  The six `memory_*` tools are split into `memory-read` and `memory-write` route groups. They appear only when the router selects them; `MEMORY_ENABLED=false` vetoes both groups, while `MEMORY_ENABLED=true` also enables the compact Memory Index in the prompt. `/memory_switch` manages that compatibility gate.
275
283
 
276
- Frontend capabilities are also split by purpose: `browser` forms `browser-debug`, whole-desktop `screenshot` is `desktop-observe`, and `dev_server` has its own ungated `background-exec` group — any process that must outlive a single tool call (dev server, inference service, watcher, log tail) belongs there rather than in `run_command`. Selecting `browser-debug` implies `background-exec`, so a weak model that only asks for the browser still gets the ability to start the server it needs to look at. Image reading lives in `read_file` (magic-byte sniffing) and remains a common read tool. The router may combine these groups with `computer-control` when a task genuinely needs both structured web diagnostics and real desktop interaction. `/fe off` is a hard veto, not a manual profile selector — it does not affect `dev_server`.
284
+ Frontend capabilities are also split by purpose: `browser` forms `browser-debug`, whole-desktop `screenshot` is `desktop-observe`, and `dev_server` has its own `background-exec` group (not controlled by `/fe`; execution still goes through permissions) — any process that must outlive a single tool call (dev server, inference service, watcher, log tail) belongs there rather than in `run_command`. Selecting `browser-debug` implies `background-exec`, so a weak model that only asks for the browser still gets the ability to start the server it needs to look at. Image reading lives in `read_file` (magic-byte sniffing) and remains a common read tool. The router may combine these groups with `computer-control` when a task genuinely needs both structured web diagnostics and real desktop interaction. `/fe off` is a hard veto, not a manual profile selector — it does not affect `dev_server`.
277
285
 
278
286
  ### Shell selection
279
287
 
@@ -323,6 +331,7 @@ dev_server stop id=srv-xxxx
323
331
  | `/model` | Two-level picker (provider → model; scoped search), plus baseURL / apiKey / context-window config; applied immediately + persisted |
324
332
  | `/effort` | Set reasoning effort off/low/medium/high/auto (e.g. `/effort high`); not sent for unrecognized models |
325
333
  | `/stats` | Session usage: cache hit rate / tiered tokens / compaction count |
334
+ | `/upgrade` | Explicit update check/status/install (`check` / `status` / `now`); no automatic startup update |
326
335
  | `/init` | Scan the project and generate `AGENTS.md` project memory (dispatched to the agent) |
327
336
  | `/theme` | Switch color theme (↑↓ · Enter, or `/theme <name>` directly) |
328
337
  | `/plan` | Switch to plan mode (read-only exploration + plan output, approve to switch to auto) |
@@ -374,9 +383,9 @@ MoCode has a **two-tier memory** model distinct from skills:
374
383
  ## Type checking
375
384
 
376
385
  ```bash
377
- npm run typecheck # tsc --noEmit
386
+ npm run typecheck # protocol/runtime + root + tests + evals
378
387
  ```
379
388
 
380
389
  ## Future extensions
381
390
 
382
- MCP tool integration, finer-grained capability locks, and a real worktree-isolated sub-agent mode. The current version is a streaming, reasoning-visible, rollback-capable terminal coding agent with 25 tools, working-notepad planning, cross-session memory, capability-aware tool scheduling, serial workspace-sharing sub-agents, and an optional desktop pet.
391
+ MCP tool integration is already supported; see the [MCP configuration guide](src/mcp/README.md). Finer-grained capability coordination and optional per-worker worktree isolation are extension directions, not guarantees of the current shared-workspace sub-agent implementation. Current tool registrations are maintained in the [built-in registry](src/tools/builtins/index.ts), and production, optional, incubating, and experimental stacks are distinguished in [stack status](docs/architecture/stack-status.md).
package/README.zh-CN.md CHANGED
@@ -38,25 +38,25 @@ mocode 自己探索代码、读写改文件、执行命令、联网查资料,以
38
38
 
39
39
  MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,内核通过受控能力平面执行真实操作,持久化认知层则让长任务和跨会话工作保持连贯。
40
40
 
41
- <p align="center"><img src="./assets/architecture/system-overview-zh-CN.svg" alt="MoCode 分层系统架构" width="100%"></p>
41
+ 实现入口:[Agent 内核](src/agent/core.ts)、[工具派发](src/agent/stages/tool-dispatcher.ts)、[工具执行](src/tools/tool-runtime.ts)。
42
42
 
43
43
  ### 自治执行循环
44
44
 
45
45
  每次模型响应都是闭环中的一步。工具调用按能力声明分类,安全读取可以并行,写操作获取规范化资源锁。工具证据除单条 hard cap 外原样进入 history,用户与模型看到同一事实。agent 没有更多工具调用时立即完成;框架不会暗中运行验证,也不会强迫追加一轮模型调用。
46
46
 
47
- <p align="center"><img src="./assets/architecture/agent-loop-zh-CN.svg" alt="MoCode 自治 Agent 执行循环" width="100%"></p>
48
-
49
47
  ### 只在真实 Context Pressure 下压缩
50
48
 
51
49
  正常会话保留完整工具证据,只维护 freshness / provenance 元数据。总上下文达到 80% 时,统一调度事件会执行已启用的 superseded、stale artifact、旧日志/搜索清理,然后始终继续 history compact;Lifecycle 不再按工具调用次数老化正文。
52
50
 
53
- <p align="center"><img src="./assets/architecture/context-engine-zh-CN.svg" alt="MoCode 上下文工程与持久化记忆架构" width="100%"></p>
51
+ 具体行为以[预算计算](src/context/budget.ts)和[压力调度](src/session/scheduler.ts)实现为准。
52
+
53
+ ### 多 Agent 协作:共享工作区,有界并发
54
54
 
55
- ### 多 Agent 并行,但不冒险共享写入
55
+ 子 agent 维护独立历史分支,可以复用父级对话前缀,工具能力不能超出父 step 委派的范围。连续的子 agent 调用按并发上限分批执行(`SUB_AGENT_CONCURRENCY` 默认 `5`,设为 `1` 时逐个派发)。
56
56
 
57
- 只读子 Agent 可以并行扇出;写任务在私有文件系统 overlay 中完成并返回结构化 ChangeSet。协调器校验 expected hash、获取规范化资源锁并安全合并冲突;是否验证以及验证范围由 agent 在主工作区自行决定。
57
+ **历史隔离不等于文件系统隔离。** 子 agent 直接写入共享工作区,并继承父级当前轮次的回滚记录;没有私有文件系统 overlay、每个 worker 独立的 worktree,也没有任务结束后的 ChangeSet 合并阶段。嵌套工具各自获取资源锁,支持的文件编辑各自执行 expected hash 校验;这些保护不意味着整个多步子任务具备事务隔离。主线接收最终摘要、状态、用量和已追踪的改动文件,而不是完整的子任务过程记录。是否验证及验证范围仍由 agent 自主决定。
58
58
 
59
- <p align="center"><img src="./assets/architecture/multi-agent-zh-CN.svg" alt="MoCode 多 Agent overlay 与 ChangeSet 协调" width="100%"></p>
59
+ 对应实现:[子 agent 执行](src/agent/spawn.ts)、[sub-agent 工具](src/tools/builtins/task.ts)、[能力感知派发](src/agent/stages/tool-dispatcher.ts)。
60
60
 
61
61
  ### 受控执行:权限门 + 能力调度
62
62
 
@@ -68,18 +68,16 @@ MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,
68
68
 
69
69
  mocode 不会在任务结束时暗中启动验证瀑布。agent 可以根据任务风险自行调用 `run_command` 跑聚焦测试、typecheck 或 build;也可以在无需额外证据时直接结束,不产生框架强制的额外轮次。
70
70
 
71
- ### 回滚时间线:每次写入都留干净撤销点
71
+ ### 回滚时间线:追踪文件改动,按轮次恢复
72
72
 
73
- 每次写入工具执行前先存一份 undo 快照。`/rollback <turnId>` 按时间逆序在 canonical 资源锁下恢复文件缓冲,不重跑模型,也不自动启动测试。读取类工具、网络副作用、二进制改动明确不在截图范围,契约里写死。
73
+ 回滚记录支持范围内的文本文件改动,包括子 agent 在父级轮次中产生的已追踪改动。`/rollback` 在规范化资源锁下恢复已记录的文件内容,不重跑模型,也不自动启动测试。它不是通用撤销机制:网络副作用、桌面操作、二进制改动及其他未追踪的副作用不在恢复范围内。中断当前轮次不会自动撤销已经完成的文件写入。
74
74
 
75
- <p align="center"><img src="./assets/architecture/rollback-flow-zh-CN.svg" alt="MoCode 回滚时间线和每轮快照流" width="100%"></p>
75
+ 快照捕获与恢复边界见[回滚存储实现](src/rollback/store.ts)。
76
76
 
77
77
  ### 上下文控制:一个真实压力线,阶段独立可选
78
78
 
79
79
  这些控制项仍可独立配置,但自动改写只有一个触发条件:校正后或原始请求占用达到 80%。该事件会运行所有已启用的 pressure 清理,然后始终继续压缩历史。`contextLifecycle` 只维护 provenance 元数据,EWMA 则让估算持续对齐 provider 实测用量。
80
80
 
81
- <p align="center"><img src="./assets/architecture/context-controls-zh-CN.svg" alt="MoCode 上下文控制:五个独立开关、观察生命周期、token 自校准" width="100%"></p>
82
-
83
81
  ### 桌宠:WebSocket 上的被动镜像
84
82
 
85
83
  可选 Electron 子包(`packages/pet-app`)用一个悬浮小角色镜像 agent 状态:单向 WebSocket 推送事件帧,`/pet quit` 完全关闭。渲染层零业务逻辑,无论桌宠是否在跑,主 agent 循环一字不改。
@@ -91,7 +89,7 @@ mocode 不会在任务结束时暗中启动验证瀑布。agent 可以根据任
91
89
  mocode 把代码层控制保持得尽量轻,把任务策略交给 agent:
92
90
 
93
91
  - **建议式工作纪律** — system prompt 只要求聚焦改动、避免重复检索、诚实报告不确定性;是否验证及验证范围由 agent 自主决定,不是完成硬门。
94
- - **透明工具失败** — 每个工具调用只执行一次,原始结构化错误直接交给 agent,由 agent 自主决定是否以及如何恢复。
92
+ - **有界重试,明确报告失败** — runtime 仅对显式声明 `idempotent` 的工具瞬时错误自动重试,最多两次;`TIMEOUT`、权限拒绝、中断和非幂等调用不自动重试。最终结构化结果交给 agent,由其决定后续恢复方式,详见[自动重试](#自动重试retryable-契约)。
95
93
  - **ask_human 卡点降级** — 仅高影响且属于用户所有权的选择才询问,其余实现细节由 agent 自主推进。
96
94
  - **五区上下文控制 + token 自校准** — 独立旋钮管理上下文压力,token 估算根据真实 provider 用量校准。
97
95
 
@@ -100,15 +98,15 @@ mocode 把代码层控制保持得尽量轻,把任务策略交给 agent:
100
98
  mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
101
99
 
102
100
  - **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
103
- - **只读工具并行执行** — 一轮里连续的只读操作(读文件、grep、glob、codegraph、联网搜索/抓取)自动并发跑,总耗时 ≈ 最慢一个,而不是逐个排队。写文件 / 改文件这类有副作用的操作仍串行,保快照顺序与数据安全。
104
- - **子 agent 分而治之** — 复杂任务可派生拥有独立历史与受限工具集的子 agent。只读 worker 可并行扇出;写 worker 在私有文件系统 overlay 中运行,返回的 ChangeSet 经过 expected hash 校验与规范化资源锁后才合并。主线只接收结构化发现,不接收过程噪声。
105
- - **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式允许执行,但不是静态“全工具”模式:每个真实用户轮先由轻量 LLM router 选择最小充分工具簇,主模型需要时可在后续 step 追加能力。
101
+ - **能力感知并发** — 连续且声明支持并行的工具(如读文件、grep、glob、联网读取)可并发执行。按资源加锁的文件改动也可同时派发,由规范化资源锁协调冲突资源,而不是把所有写入全局串行化。进程类及未声明并行能力的工具保持各自的串行执行约束。
102
+ - **子 agent 分而治之** — 子任务拥有独立历史分支,工具权限不超过父 step 的能力上限;按并发上限分批运行,直接写入共享工作区并共用父级回滚轮次。主线接收最终摘要及执行元数据;没有每个 worker 独立的文件系统隔离或合并阶段。
103
+ - **计划 / 执行双模式** — `plan` 模式下探查并产出计划,不修改项目文件、不跑 shell 命令、不派生子 agent;内部会话和计划记录仍可持久化。`auto` 模式允许执行,但不是静态“全工具”模式:每个真实用户轮先由轻量 LLM router 选择最小充分工具簇,主模型需要时可在后续 step 追加能力。
106
104
  - **统一压力驱动压缩** — 正常 history 保留完整工具证据;达到 80% 后由一次调度事件运行所有已启用的清理,并始终继续 history 摘要。`/context` 显示实时用量,`/compact` 仍是用户显式覆盖。
107
- - **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
105
+ - **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;可通过 `AUTO_REFLECT=true` 开启后台反思,从对话中挖掘值得记住的事实(默认关闭)。记忆可增删改、带召回衰减。
108
106
  - **会话记事本(notes.md)** — 复杂多步任务(≥3 处文件改动 / ≥5 步工具调用)时,agent 在 `.mocode/sessions/<sessionId>/notes.md` 维护一个工作记事本(落盘抗压缩),可记录中间发现、设计决策、待验证问题和结构化计划。执行计划由专用 `plan_update` 工具维护——三态步骤机(`pending`/`in_progress`/`completed`,同一时刻至多一个 `in_progress`),全部完成自动结算为 `## Done:`。活跃 plan 在压缩后重注入系统提示、notes.md 一变就重同步进上下文,若连续多步未更新还会有温和提醒。TUI 状态栏实时显示进度 chip:`plan: [标题] (3/7) ▸ [当前步]`。
109
- - **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
107
+ - **可中断、可回滚** — Ctrl+C 将中断信号传给模型请求及支持取消的运行中工具;历史恢复到最近已提交工具批次的检查点,而不是无条件丢弃整轮。已经完成的文件写入仍保留,需显式撤销;`/rollback` 可从轮次快照恢复已追踪文件,逐个选择「保留/撤销」,不依赖 git。
110
108
  - **输入安全网** — 长 prompt 不再怕误按 Enter:`Ctrl+G` 弹出 TUI 内「输入面板」(记事本式编辑,Enter=换行、软换行、选区、复制/剪切/粘贴、撤销,Ctrl+S 填回输入框不自动发送);`Ctrl+R`/`Ctrl+P` 模糊搜索历史输入(Enter 只回填不发送);长文本误发后撤回窗口自动放宽到 2 秒且任意键可撤回。
111
- - **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
109
+ - **文件路径边界** — 内置文件读写拒绝越出 `SANDBOX_ROOT`(默认工作目录)的路径,包括路径穿越和软链出圈。这是文件工具的路径保护,不是针对 shell 命令、MCP server 或桌面输入的操作系统级沙箱;记忆与 skills 可以按设计访问项目外的配置位置。允许高风险工具前应审查权限,详见[沙箱策略](src/sandbox/policy.ts)。
112
110
  - **Computer Use(高危,仅明确 GUI 意图时路由)** — 请求确实需要真实鼠标/键盘交互时,router 才可暴露 `computer-control`,并把每次动作后的截图回灌模型。`/cu off`(或 `MOCODE_COMPUTER_USE_ENABLED=false`)是硬否决;`/cu on` 仅允许按需路由,不会让工具常驻。它会绕过文件沙箱,**强烈建议只在 VM / 沙箱 / 专用测试机里使用**。每个动作仍走权限门,plan 模式永远屏蔽。Windows 首发,macOS/Linux 待接入。
113
111
 
114
112
  ## 特性
@@ -141,7 +139,7 @@ npm install -g mocode-ai
141
139
 
142
140
  装完即得 `mocode` 命令。不想全局装也可免装直跑:`npx mocode-ai`。
143
141
 
144
- > mocode 启动时自动检测新版本,后台 `npm i -g mocode-ai@latest` 自更新——下次启动生效,零启动延迟、断网 / 失败静默。开发态 `npm start`(tsx 跑 `.ts`)不触发。
142
+ > mocode 启动时不查询 registry,也不自动安装更新。需要时显式使用 `/upgrade check`、`/upgrade status` 或 `/upgrade now` 检查、查看状态或升级;源码/tsx 开发模式禁止真正执行安装。实现见[升级命令](src/commands/upgrade.ts)。
145
143
 
146
144
  ### 从源码运行(开发 / 贡献)
147
145
 
@@ -152,7 +150,23 @@ npm install
152
150
  npm start
153
151
  ```
154
152
 
155
- 源码经 tsx 直接跑,无构建步骤。改完代码需重启 `npm start` 生效(tsx 启动时加载模块,不热更新)。依赖:`openai`、`dotenv`、`fast-glob`(运行时);`tsx`、`typescript`、`@types/node`(开发)。
153
+ 源码经 tsx 直接运行,无需先构建。修改代码后需重启 `npm start` 生效(tsx 启动时加载模块,不热更新)。请在仓库根目录安装依赖,让 npm 处理 workspaces;完整依赖和开发命令以 [package.json](package.json) 为准,不在此重复维护清单。
154
+
155
+ ### 技术栈状态与贡献边界
156
+
157
+ 生产主路径是 TypeScript CLI;`packages/work-app` 处于孵化阶段,`packages/pet-app` 是可选组件,`rust/` 是非关键路径的实验性 TUI,设有晋升或归档期限。参见:
158
+
159
+ - [技术栈状态与责任人](docs/architecture/stack-status.md)
160
+ - [架构决策](docs/adr/README.md)
161
+ - [贡献指南](CONTRIBUTING.md)
162
+ - [维护与排障手册](docs/runbooks/README.md)
163
+ - [Rust 实验状态](rust/README.md)
164
+
165
+ 不要导入其他 package 的 `src/` 或内部 `dist/` 布局;应用通过 package exports 和公开的 `mocode-agent-host` bin 契约消费能力。
166
+
167
+ ### 文档与实现同步
168
+
169
+ 修改执行、权限、并发、回滚或升级行为时,应按本页对应的源码入口同步更新 `README.md` 和 `README.zh-CN.md`;架构图与当前行为一致后才嵌入。依赖与工具清单分别以 package manifests 和内置工具注册表为准,发布边界以技术栈状态页为准,不把设计提案或旧架构图当作已交付保证。
156
170
 
157
171
  ## 配置
158
172
 
@@ -208,6 +222,8 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
208
222
  | `MOCODE_LIFECYCLE` | 只维护 provenance 元数据,不按次数改写正文 | `true` |
209
223
  | `MAX_STEPS` | 每轮 Agent 循环最大步数(仅防无限循环) | `1000` |
210
224
  | `SUB_AGENT_MAX_STEPS` | 子 Agent 循环安全上限,默认与主 Agent 一致 | `1000` |
225
+ | `SUB_AGENT_CONCURRENCY` | 每批子 agent 调用的并发上限;设为 `1` 时逐个派发 | `5` |
226
+ | `SUB_AGENT_MAX_DEPTH` | 递归委派深度上限 | `3` |
211
227
  | `SANDBOX_ROOT` | 沙箱根目录(文件操作边界;未配则用 cwd 兜底) | 无 |
212
228
  | `MOCODE_SUBAGENT_ENABLED` | 设 `false` 硬禁用 `orchestration`;unset/`true` 允许按需路由 | 未设置 |
213
229
  | `MOCODE_FRONTEND_TOOLS_ENABLED` | 设 `false` 硬禁用 `browser-debug` / `desktop-observe`(不影响 `background-exec`);unset/`true` 允许路由 | 未设置 |
@@ -226,7 +242,7 @@ mocode --resume <id> # 续接指定会话
226
242
  mocode config # 改配置
227
243
  ```
228
244
 
229
- 从源码跑则用 `npm start`(等价于 `mocode`,但不触发自更新)。
245
+ 从源码运行使用 `npm start`。源码和安装版启动时都不会自动检查更新;`/upgrade` 由用户显式触发,源码/tsx 模式禁止真正执行安装。
230
246
 
231
247
  进入 REPL 后直接对话。启动即进全屏 TUI,显示横幅(模型 / 后端 / 工作目录 / 工具列表)。回复流式打印,思考段实时可见后折叠。
232
248
 
@@ -234,7 +250,9 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
234
250
 
235
251
  ## 工具
236
252
 
237
- 每个真实用户轮都会先经过受约束的 LLM router。九个公共工具始终可用(`read_file`、`glob`、`grep`、`web_search`、`web_fetch`、`plan_update`、`note_append`、`ask_human`、`use_skill`);写文件、shell 调试、浏览器调试、桌面观察/控制、记忆、编排和 MCP 作为可组合工具簇按需选择。初始能力不足时,主模型必须单独调用 `add_tool_groups`,新增 schema 从下一 step 生效。路由失败只继承上一轮工具簇(或仅公共工具),绝不回退到全工具。
253
+ 每个真实用户轮都会先经过受约束的 LLM router。公共工具包括 `read_file`、`glob`、`grep`、`web_search`、`web_fetch`、`plan_update`、`note_append`、`ask_human`、`use_skill`;写文件、shell 调试、浏览器调试、桌面观察/控制、记忆、编排和 MCP 作为可组合工具簇按需选择。初始能力不足时,主模型通过 `add_tool_groups` 请求追加能力,新增 schema 从下一 step 生效。路由失败只继承上一轮工具簇(或仅公共工具),绝不回退到全工具。
254
+
255
+ 以下表格列举部分能力,不是完整工具清单。当前注册及能力声明见[内置工具注册表](src/tools/builtins/index.ts),实际暴露由 [ToolPolicy](src/tools/policy.ts) 控制。Codegraph 查询在安装了对应 skill/CLI 时使用,不是独立的内置 `codegraph` 工具。
238
256
 
239
257
  | 工具 | 作用 |
240
258
  | ------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -246,20 +264,22 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
246
264
  | `dev_server` | 启动/查看/读日志/停止**任意需跨调用存活的后台进程**(dev server、推理服务、watcher、日志尾随) |
247
265
  | `browser` | Playwright 驱动真实 Chromium:导航 / 点击 / 填表 / 取文本 / 截图 / 控制台诊断 |
248
266
  | `glob` | 按 glob 模式找文件(排除 node_modules/.git) |
249
- | `grep` | 内容正则搜索,纯 JS 实现,不依赖 `rg`;`context=N` 内联返回邻居行并保留原始缩进,命中后基本不用再 read_file 一次 |
250
- | `codegraph` | 已建 `.codegraph/` 索引时,查代码符号源码与调用链(比 read_file/grep 更准更省) |
267
+ | `grep` | 内容正则搜索,以 ripgrep 为快速路径(随 `@vscode/ripgrep` 分发,尊重 `.gitignore`、跳过二进制),找不到 `rg` 时才回退纯 JS 扫描;`context=N` 内联返回邻居行并保留原始缩进,命中后基本不用再 read_file 一次 |
251
268
  | `web_search` | 联网搜索(AnySearch),返回标题/URL/摘要/正文 |
252
269
  | `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本;带全套浏览器拟真头,瞬时失败(429/5xx/网络抖动)自动退避重试,可选纯文本代理回退 |
253
270
  | `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
254
271
  | `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
255
272
  | `plan_update` | 记录/更新会话执行计划(notes.md 的 `## Plan:` 段);三态步骤机,同一时刻至多一个 in_progress,全部完成自动结算为 `## Done:` |
256
- | `sub-agent` | 派生工具权限不超过父级 snapshot 的隔离子 Agent;只读任务可并发,写任务通过 overlay + ChangeSet 安全合并 |
273
+ | `sub-agent` | 派生独立历史、工具权限不超过父 step 的子 agent;有界并发,共享工作区和回滚轮次 |
257
274
 
275
+ | 记忆工具 | 作用 |
276
+ | -------- | ---- |
258
277
  | `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
259
278
  | `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
260
279
  | `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
261
280
  | `memory_update` | 原地改一条记忆(id 不变;纠正过时事实 / 改摘要 / 改 pin) |
262
281
  | `memory_forget` | 遗忘记忆:默认归档(可复活),`mode=delete` 硬删(pinned 拒删) |
282
+ | `memory_graph` | 遍历邻居、查找路径、添加三元组或查看图谱统计;关键词搜索由 `memory_search` 提供 |
263
283
 
264
284
  ### 前端 / UI 闭环
265
285
 
@@ -277,7 +297,7 @@ dev_server stop id=srv-xxxx
277
297
  - 两者在 plan 模式下均被禁用;mocode 退出时会树杀后台进程并关闭浏览器。
278
298
  - 浏览器二进制不随 npm 包分发,首次使用前需 `npx playwright install chromium`。
279
299
 
280
- 前端能力按用途拆分:`browser` 属于 `browser-debug`,整桌面截图 `screenshot` 属于 `desktop-observe`,而 `dev_server` 独立成**无 gate 的 `background-exec` 簇** —— 任何需要跨工具调用存活的进程(dev server、推理/模型服务、watcher、日志尾随)都归它,而不是塞进 `run_command`。选中 `browser-debug` 会**蕴含**激活 `background-exec`:弱模型只想到要浏览器时,也能拿到「先把服务起起来」的能力(半套能力比多一套能力更糟)。图片读取(`read_file` 魔数分流)则始终是公共只读能力。任务同时需要结构化网页诊断与真实桌面交互时,router 可再组合 `computer-control`。`/fe off` 是硬否决,不是手动 profile 选择器——它不影响 `dev_server`。
300
+ 前端能力按用途拆分:`browser` 属于 `browser-debug`,整桌面截图 `screenshot` 属于 `desktop-observe`,而 `dev_server` 独立成 **`background-exec` 簇**(不受 `/fe` 开关控制,但执行仍需经过权限检查) —— 任何需要跨工具调用存活的进程(dev server、推理/模型服务、watcher、日志尾随)都归它,而不是塞进 `run_command`。选中 `browser-debug` 会**蕴含**激活 `background-exec`:弱模型只想到要浏览器时,也能拿到「先把服务起起来」的能力(半套能力比多一套能力更糟)。图片读取(`read_file` 魔数分流)则始终是公共只读能力。任务同时需要结构化网页诊断与真实桌面交互时,router 可再组合 `computer-control`。`/fe off` 是硬否决,不是手动 profile 选择器——它不影响 `dev_server`。
281
301
 
282
302
  ### Shell 选择器
283
303
 
@@ -313,6 +333,7 @@ dev_server stop id=srv-xxxx
313
333
  | `/model` | 两级面板切换模型(先选厂商再选模型;顶层按厂商名、厂商内按模型名过滤);也可配置 baseURL / apiKey / 上下文窗口,即时生效 + 持久化 |
314
334
  | `/effort` | 设置思考强度 off/low/medium/high/auto(如 `/effort high`);未识别模型不下发参数 |
315
335
  | `/stats` | 本会话用量:缓存命中率 / 分层 token / 压缩次数 |
336
+ | `/upgrade` | 显式检查更新、查看状态或安装(`check` / `status` / `now`);启动时不自动更新 |
316
337
  | `/init` | 扫描项目生成 `AGENTS.md` 项目记忆(发给 agent 执行) |
317
338
  | `/theme` | 切换颜色主题(↑↓ · Enter,或 `/theme <name>` 直切) |
318
339
  | `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
@@ -364,9 +385,9 @@ mocode 的**双层记忆**模型,跟 Skills 是两件事:
364
385
  ## 类型检查
365
386
 
366
387
  ```bash
367
- npm run typecheck # tsc --noEmit
388
+ npm run typecheck # protocol/runtime + 根项目 + tests + evals
368
389
  ```
369
390
 
370
391
  ## 可后续扩展
371
392
 
372
- MCP 工具集成、更细粒度的 capability 资源锁、真·worktree 隔离的子 agent 模式。当前版本已是流式、思考可见、可回滚的终端编码 agent:内置文本与视觉工具集、工作记事本规划、跨会话记忆、能力感知工具调度、共享工作区串行子 agent、可选桌宠。
393
+ MCP 工具集成已经支持,配置见 [MCP 指南](src/mcp/README.md)。更细粒度的能力协调、可选的每 worker 独立 worktree 是后续扩展方向,不是当前共享工作区子 agent 已具备的保证。现有工具以[内置工具注册表](src/tools/builtins/index.ts)为准,生产、可选、孵化和实验技术栈的区别见[技术栈状态](docs/architecture/stack-status.md)。
@@ -495,7 +495,7 @@ ${buildVoiceSection()}
495
495
  - Stop immediately when no more tools are needed; give conclusions directly.
496
496
  - **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.
497
497
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
498
- - 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.
498
+ - Close with a brief explanation of what you did and why, not just a verdict, so the user doesn't have to re-read the tool trace: when files changed, group them by file or logical change — what changed and the behavior/fix it delivers, one or two lines each with "path:line" references, noting renames/deletions and skipping purely mechanical edits; for investigation or analysis, state the conclusion and the key evidence. Say success when successful, where you're stuck when failing, and anything skipped. Keep it concise.
499
499
  - Definition of done: never declare a multi-feature request finished just because the code exists. Each distinct user request listed in the plan must be actually verified (the relevant check run / observed result), not merely implemented; mark plan steps \`[x]\` only after that verification. If the user supplied an acceptance list, every item on it must pass. Leave unverified items explicitly listed as pending instead of collapsing them into "done".`;
500
500
  // 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
501
501
  // ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
@@ -7,9 +7,6 @@ import { pruneStaleArtifacts, refreshArtifactFreshness } from '../context/artifa
7
7
  import { pruneSuperseded } from '../context/relevance.js';
8
8
  import { clearRetrievableResults } from '../context/clearing.js';
9
9
  import { createAgeAwareEncodingState } from '../context/age-aware.js';
10
- function atPressure(report) {
11
- return Math.max(report.rawTotal, report.total) >= report.window * DEFAULT_BUDGET_POLICY.pressureTriggerRatio;
12
- }
13
10
  function emptyPressure(report) {
14
11
  return {
15
12
  triggered: false,
@@ -235,7 +235,7 @@ export class SessionStore {
235
235
  load(id) {
236
236
  const currentPath = this.sessionPath(id);
237
237
  const legacyPath = path.join(this.sessionsRoot, `${id}.json`);
238
- let source = existsSync(currentPath) ? currentPath : legacyPath;
238
+ const source = existsSync(currentPath) ? currentPath : legacyPath;
239
239
  // 活会话与 legacy 单文件都没有:回退从归档 gz 取回(30-90 天的会话仍可 resume)。
240
240
  if (!existsSync(source)) {
241
241
  const archived = loadArchivedSession(this.sessionsRoot, id);
@@ -1,6 +1,11 @@
1
1
  import fg from 'fast-glob';
2
2
  import { IGNORE } from '../constants.js';
3
3
  import { getSandboxRoot, isInsideRoot } from '../../sandbox/index.js';
4
+ /**
5
+ * glob 超时墙。纯文件名枚举正常是毫秒~秒级;超时只发生在网络盘/断开的 UNC 挂载等病态
6
+ * 场景。此时杀掉枚举报错,而不是让 spinner 无限期挂住。
7
+ */
8
+ export const GLOB_TIMEOUT_MS = 30_000;
4
9
  // ---------- glob ----------
5
10
  export const globTool = {
6
11
  name: 'glob',
@@ -13,23 +18,66 @@ export const globTool = {
13
18
  },
14
19
  required: ['pattern'],
15
20
  },
16
- async execute(args) {
21
+ async execute(args, ctx) {
17
22
  const pattern = String(args.pattern);
18
23
  const cwd = getSandboxRoot() ?? process.cwd();
19
- const files = (await fg(pattern, {
20
- cwd,
21
- onlyFiles: true,
22
- dot: true,
23
- ignore: IGNORE,
24
- followSymbolicLinks: false, // 不跟随软链目录,防经软链列出牢外文件
25
- throwErrorOnBrokenSymbolicLink: false,
26
- })).filter((f) => isInsideRoot(f)); // 后置兜底:仅留牢内
27
- if (files.length === 0)
28
- return '无匹配文件';
29
- const shown = files.slice(0, 200);
30
- let out = shown.join('\n');
31
- if (files.length > 200)
32
- out += `\n... (共 ${files.length} 个,仅显示前 200)`;
33
- return out;
24
+ // 组合外部 Ctrl+C 信号与超时墙:fast-glob 3.3 原生支持 AbortSignal,中止后枚举立即失败。
25
+ const controller = new AbortController();
26
+ const external = ctx?.signal;
27
+ const onExternal = () => controller.abort();
28
+ if (external) {
29
+ if (external.aborted)
30
+ onExternal();
31
+ else
32
+ external.addEventListener('abort', onExternal, { once: true });
33
+ }
34
+ let timedOut = false;
35
+ const timer = setTimeout(() => {
36
+ timedOut = true;
37
+ controller.abort();
38
+ }, GLOB_TIMEOUT_MS);
39
+ try {
40
+ // fast-glob 3.3 的 signal 选项运行时不生效(实测),用 Promise.race 做超时/中断墙:
41
+ // 触发后不再等待枚举——纯只读 readdir 会自行跑完,用户面即时解锁。
42
+ const fgPromise = fg(pattern, {
43
+ cwd,
44
+ onlyFiles: true,
45
+ dot: true,
46
+ ignore: IGNORE,
47
+ followSymbolicLinks: false, // 不跟随软链目录,防经软链列出牢外文件
48
+ throwErrorOnBrokenSymbolicLink: false,
49
+ });
50
+ const cancelled = new Promise((resolve) => {
51
+ if (controller.signal.aborted)
52
+ resolve('cancelled');
53
+ else
54
+ controller.signal.addEventListener('abort', () => resolve('cancelled'), { once: true });
55
+ });
56
+ const files = await Promise.race([fgPromise, cancelled]);
57
+ if (files === 'cancelled') {
58
+ if (timedOut) {
59
+ return {
60
+ status: 'error',
61
+ code: 'TIMEOUT',
62
+ retryable: false,
63
+ output: `错误:glob 超过 ${GLOB_TIMEOUT_MS / 1000}s 超时墙已被中止(确认无网络盘挂载,或收窄 pattern 后重试)`,
64
+ };
65
+ }
66
+ // 外部 Ctrl+C:中断串由 tool-runtime 见 signal.aborted 归一为 aborted。
67
+ return '已中断';
68
+ }
69
+ const safeFiles = files.filter((f) => isInsideRoot(f)); // 后置兜底:仅留牢内
70
+ if (safeFiles.length === 0)
71
+ return '无匹配文件';
72
+ const shown = safeFiles.slice(0, 200);
73
+ let out = shown.join('\n');
74
+ if (safeFiles.length > 200)
75
+ out += `\n... (共 ${safeFiles.length} 个,仅显示前 200)`;
76
+ return out;
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ external?.removeEventListener('abort', onExternal);
81
+ }
34
82
  },
35
83
  };
@@ -2,69 +2,168 @@ import { readFile } from 'node:fs/promises';
2
2
  import fg from 'fast-glob';
3
3
  import { IGNORE, MAX_RESULTS } from '../constants.js';
4
4
  import { isProbablyBinary } from '../../attachments/image.js';
5
- // 二进制文件探测统一走 attachments/image.ts 的 isProbablyBinary(与 read_file 同源同口径,
6
- // 避免两处正则漂移):头部含 C0 控制字符即跳过。否则 grep 扫到 SQLite/压缩文件等会产出
7
- // 「单行数 KB + 控制字符」的匹配行,这类行进 TUI 展开后被终端 auto-wrap,物理行与缓冲行
8
- // 失配导致整屏错乱。
9
5
  import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.js';
10
- /** 单文件读取上限:超过即跳过(压缩产物 / 生成物 / 数据 dump)。
11
- * 逐文件全量 readFile 进内存是 grep 的固有成本,不设上限时一个 200MB 的 bundle
12
- * 就能把整轮 grep 拖死并撑爆内存。跳过的文件数会在结果尾部报出来,不静默吞。 */
6
+ import { runRipgrep } from '../ripgrep-client.js';
7
+ /**
8
+ * grep 超时墙。
9
+ *
10
+ * ripgrep 正常仓库都是秒级,超时只发生在网络盘挂死这类病态场景——此时与其让 spinner
11
+ * 无限期冻住,不如杀掉报错让模型换路径(收窄 glob)。比 run_command 默认 120s 更严:
12
+ * 全库搜索不该有 rg 跑满 2 分钟还出不来的结果。
13
+ */
14
+ export const GREP_TIMEOUT_MS = 120_000;
15
+ /** Node 兜底路径的单文件读取上限(ripgrep 流式读取不需要这个闸门,仅兜底用)。 */
13
16
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
14
- /** 单条 body 行渲染上限(字符):minified JS / 单行 JSON 截断加 …,
15
- * 既防 TUI auto-wrap 错乱,也防一行吃掉整个 MAX_RESULTS 预算。 */
17
+ /** 单条 body 行渲染上限(字符):minified JS / 单行 JSON 截断加 …,防 TUI auto-wrap 错乱。 */
16
18
  const MAX_BODY_LINE_CHARS = 400;
17
- /** context 参数上限:邻居行是 ×(1+2C) 放大,给到 10 已远超「看清一段逻辑」的需要。 */
19
+ /** context 参数上限:邻居行是 ×(1+2C) 放大,给到 10 已远超需要。 */
18
20
  const MAX_CONTEXT_LINES = 10;
19
- /** 超长行截断(保留头部:grep 的价值在行首标识与缩进)。 */
20
21
  function clipLine(line) {
21
22
  if (line.length <= MAX_BODY_LINE_CHARS)
22
23
  return line;
23
24
  return `${line.slice(0, MAX_BODY_LINE_CHARS)}…(+${line.length - MAX_BODY_LINE_CHARS} 字符)`;
24
25
  }
25
26
  /**
26
- * 把命中行号 + context 半径渲染成 body 行。
27
- *
28
- * 输出格式契约(context/encoders/search.ts 依赖前两个前缀做 Cold 折叠):
27
+ * 把命中行 + context 行渲染成 body。输出格式契约(context/encoders/search 依赖前缀做折叠):
29
28
  * ` L<n>: <原文>` 命中行(冒号)
30
- * ` L<n>- <原文>` 上下文行(连字符,对齐 ripgrep 的 `:` / `-`)
31
- * ` --` 不相邻分块之间的分隔(对齐 ripgrep)
32
- * 原文**不 trim**:缩进是代码结构信息,trim 掉后模型看不出嵌套层级,只能再发一次
33
- * read_file 去确认——那正是 context 参数要消灭的往返。
29
+ * ` L<n>- <原文>` 上下文行(连字符)
30
+ * ` --` 不相邻分块之间的分隔
31
+ * 原文不 trim:缩进是代码结构信息。
34
32
  */
35
- function renderBodies(lines, lineNos, maxPerFile, context) {
36
- const shown = lineNos.slice(0, maxPerFile);
33
+ function renderFileBodies(file, maxPerFile) {
34
+ const shown = file.matches.slice(0, maxPerFile);
37
35
  if (shown.length === 0)
38
36
  return [];
39
- const matched = new Set(shown);
40
- // 命中行 ± context 的并集,排序后按连续块渲染。
41
- const wanted = new Set();
42
- for (const n of shown) {
43
- for (let i = n - context; i <= n + context; i++) {
44
- if (i >= 1 && i <= lines.length)
45
- wanted.add(i);
46
- }
47
- }
37
+ const matched = new Set(shown.map((e) => e.line));
38
+ const byLine = new Map();
39
+ for (const e of shown)
40
+ byLine.set(e.line, e);
41
+ for (const e of file.contexts)
42
+ byLine.set(e.line, e);
48
43
  const out = [];
49
44
  let previous = 0;
50
- for (const n of [...wanted].sort((a, b) => a - b)) {
45
+ for (const n of [...byLine.keys()].sort((a, b) => a - b)) {
51
46
  if (previous !== 0 && n !== previous + 1)
52
47
  out.push(' --');
53
48
  previous = n;
54
- out.push(` L${n}${matched.has(n) ? ':' : '-'} ${clipLine(lines[n - 1])}`);
49
+ const event = byLine.get(n);
50
+ out.push(` L${n}${matched.has(n) ? ':' : '-'} ${clipLine(event.text)}`);
55
51
  }
56
52
  return out;
57
53
  }
54
+ /** 把聚合后的文件事件渲染成工具输出(配额/截断口径与旧实现一致)。 */
55
+ function renderResults(files, maxPerFile, context, truncated) {
56
+ const hits = [];
57
+ let budgetTruncated = truncated;
58
+ for (const file of files) {
59
+ const bodyCost = Math.min(file.matches.length, maxPerFile) * (1 + 2 * context);
60
+ const totalCost = file.matches.length + bodyCost;
61
+ if (hits.length >= MAX_RESULTS || totalCost > MAX_RESULTS * 4) {
62
+ if (hits.length < MAX_RESULTS)
63
+ hits.push({ file, bodies: [] });
64
+ budgetTruncated = true;
65
+ continue;
66
+ }
67
+ hits.push({ file, bodies: renderFileBodies(file, maxPerFile) });
68
+ }
69
+ const out = [];
70
+ let totalShown = 0;
71
+ for (const h of hits) {
72
+ out.push(`${h.file.path}: ${h.file.matches.length} 处匹配,行号 [${h.file.matches.map((e) => e.line).join(', ')}]`);
73
+ if (h.bodies.length > 0)
74
+ out.push(...h.bodies);
75
+ else
76
+ out.push(' (body 已折叠,见上方行号列表 → read_file 精读)');
77
+ totalShown += h.file.matches.length + h.bodies.length;
78
+ if (totalShown >= MAX_RESULTS) {
79
+ out.push(`...(结果达到 ${MAX_RESULTS} 条上限${context > 0 ? ',context 行同样计入;要更全就减小 context 或收窄 glob' : ''})`);
80
+ break;
81
+ }
82
+ }
83
+ if (budgetTruncated)
84
+ out.push('...(仍有更多匹配未展示,缩小 glob 或收窄正则)');
85
+ return out.join('\n');
86
+ }
87
+ /**
88
+ * Node 兜底:rg 二进制不可用(可选依赖缺失且 PATH 无 rg)时走旧路径。
89
+ * 与旧实现的唯一差异——每两个文件之间检查 abort 信号,使 Ctrl+C / 超时墙能即时打断。
90
+ */
91
+ async function nodeFallback(pattern, glob, maxPerFile, context, signal, externalSignal) {
92
+ let re;
93
+ try {
94
+ re = new RegExp(pattern);
95
+ }
96
+ catch (e) {
97
+ return `错误:非法正则 ${pattern}: ${e instanceof Error ? e.message : String(e)}`;
98
+ }
99
+ const cwd = getSandboxRoot() ?? process.cwd();
100
+ const entries = (await fg(glob, {
101
+ cwd,
102
+ onlyFiles: true,
103
+ dot: true,
104
+ ignore: IGNORE,
105
+ stats: true,
106
+ followSymbolicLinks: false,
107
+ throwErrorOnBrokenSymbolicLink: false,
108
+ })).filter((entry) => isInsideRoot(entry.path)); // 后置兜底:仅留牢内
109
+ const files = [];
110
+ let scanned = 0;
111
+ let skippedTooLarge = 0;
112
+ const abortNote = () => externalSignal?.aborted
113
+ ? { status: 'aborted', code: 'ABORTED', output: '已中断' }
114
+ : {
115
+ status: 'error',
116
+ code: 'TIMEOUT',
117
+ retryable: false,
118
+ output: `错误:grep 超过 ${GREP_TIMEOUT_MS / 1000}s 超时墙已被中止(收窄 glob 后重试)`,
119
+ };
120
+ for (let fi = 0; fi < entries.length; fi++) {
121
+ const entry = entries[fi];
122
+ if ((fi & 31) === 0 && signal.aborted)
123
+ return abortNote();
124
+ if (entry.stats && entry.stats.size > MAX_FILE_BYTES) {
125
+ skippedTooLarge++;
126
+ continue;
127
+ }
128
+ let content;
129
+ try {
130
+ content = await readFile(jailResolve(entry.path), 'utf8');
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ scanned++;
136
+ if (isProbablyBinary(content))
137
+ continue;
138
+ const lines = content.split(/\r?\n/);
139
+ const matches = [];
140
+ for (let i = 0; i < lines.length; i++) {
141
+ if (re.test(lines[i]))
142
+ matches.push({ line: i + 1, text: lines[i] });
143
+ }
144
+ if (matches.length > 0)
145
+ files.push({ path: entry.path, matches, contexts: [] });
146
+ }
147
+ if (signal.aborted)
148
+ return abortNote();
149
+ if (files.length === 0) {
150
+ const note = skippedTooLarge ? `,跳过 ${skippedTooLarge} 个超过 2MiB 的文件` : '';
151
+ return `无匹配(扫描了 ${scanned} 个文件${note})`;
152
+ }
153
+ // 兜底路径无 context 事件来源(rg 缺失),context 参数退化为只给命中行。
154
+ const out = renderResults(files, maxPerFile, 0, false);
155
+ return skippedTooLarge ? `${out}\n...(跳过 ${skippedTooLarge} 个超过 2MiB 的文件)` : out;
156
+ }
58
157
  // ---------- grep ----------
59
158
  export const grepTool = {
60
159
  name: 'grep',
61
- description: 'Search file contents by regex (recursive, excludes node_modules/.git/dist).\n' +
160
+ description: 'Search file contents by regex (recursive; powered by ripgrep: respects .gitignore, skips binaries).\n' +
62
161
  'Output: per-file header "<path>: N matches, lines [l1, l2, ...]" plus rendered lines with ORIGINAL INDENTATION kept.\n' +
63
- 'Pass context=2..5 to include neighbouring lines inline (like ripgrep -C) instead of following each hit with a read_file round-trip. Use read_file when you need a whole region or exact edit text.',
162
+ '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.',
64
163
  parameters: {
65
164
  type: 'object',
66
165
  properties: {
67
- pattern: { type: 'string', description: 'Regular expression' },
166
+ pattern: { type: 'string', description: 'Regular expression (ripgrep/Rust regex syntax)' },
68
167
  glob: { type: 'string', description: 'Optional, restrict to a file glob, e.g. *.ts' },
69
168
  max_per_file: {
70
169
  type: 'integer',
@@ -73,106 +172,61 @@ export const grepTool = {
73
172
  context: {
74
173
  type: 'integer',
75
174
  description: 'Neighbouring lines to include around each match, like ripgrep -C (default 0, max 10). ' +
76
- 'Output grows ~x(1+2*context) and counts against the same result budget, so keep it small.',
175
+ 'Output grows ~x(1+2*context), so keep it small.',
77
176
  },
78
177
  },
79
178
  required: ['pattern'],
80
179
  },
81
- async execute(args) {
180
+ async execute(args, ctx) {
82
181
  const pattern = String(args.pattern);
83
- const g = String(args.glob ?? '**/*');
182
+ const glob = String(args.glob ?? '**/*');
84
183
  const maxPerFile = Math.min(Math.max(Number(args.max_per_file ?? 15), 1), 50);
85
184
  const contextRaw = Number(args.context ?? 0);
86
185
  const context = Number.isFinite(contextRaw) ? Math.min(Math.max(Math.trunc(contextRaw), 0), MAX_CONTEXT_LINES) : 0;
87
- let re;
88
- try {
89
- re = new RegExp(pattern);
90
- }
91
- catch (e) {
92
- return `错误:非法正则 ${pattern}: ${e instanceof Error ? e.message : String(e)}`;
186
+ // 组合外部 Ctrl+C 信号与超时墙:任一触发都中止 rg 子进程。
187
+ const controller = new AbortController();
188
+ const external = ctx?.signal;
189
+ const onExternal = () => controller.abort();
190
+ if (external) {
191
+ if (external.aborted)
192
+ onExternal();
193
+ else
194
+ external.addEventListener('abort', onExternal, { once: true });
93
195
  }
94
- const cwd = getSandboxRoot() ?? process.cwd();
95
- // stats:true 让 fast-glob 顺手带回 size,免为体积闸门再付一次 stat 系统调用。
96
- const entries = (await fg(g, {
97
- cwd,
98
- onlyFiles: true,
99
- dot: true,
100
- ignore: IGNORE,
101
- stats: true,
102
- followSymbolicLinks: false, // 不跟随软链目录,防经软链扫到牢外文件
103
- throwErrorOnBrokenSymbolicLink: false,
104
- })).filter((entry) => isInsideRoot(entry.path)); // 后置兜底:仅留牢内
105
- const hits = [];
106
- let scanned = 0;
107
- let skippedTooLarge = 0;
108
- let truncated = false;
109
- for (const entry of entries) {
110
- const f = entry.path;
111
- // 体积闸门:压缩产物 / 数据 dump 读进来只会撑爆内存并产出无意义超长行。
112
- if (entry.stats && entry.stats.size > MAX_FILE_BYTES) {
113
- skippedTooLarge++;
114
- continue;
115
- }
116
- let content;
117
- try {
118
- // jailResolve:realpath 化,防「牢内文件软链→牢外」的内容泄露;越界/不可读均 catch 跳过
119
- content = await readFile(jailResolve(f), 'utf8');
120
- }
121
- catch {
122
- continue; // 跳过无法读的文件(二进制/权限/沙箱越界)
123
- }
124
- scanned++;
125
- // 二进制文件(如 .codegraph/codegraph.db 这类 SQLite)跳过:其「行」是数 KB 的
126
- // 序列化记录 + 控制字符,匹配行对 LLM 无意义,还会污染 TUI 展开渲染。
127
- if (isProbablyBinary(content))
128
- continue;
129
- const lines = content.split(/\r?\n/);
130
- const lineNos = [];
131
- for (let i = 0; i < lines.length; i++) {
132
- if (re.test(lines[i]))
133
- lineNos.push(i + 1);
134
- }
135
- if (lineNos.length === 0)
136
- continue;
137
- // 全局配额:行号列表总是计入,body 成本按「命中行数 ×(1+2×context)」计入 ——
138
- // context 是乘法放大,不按实际渲染量计就会让带 ±5 邻居的 grep 静默撑爆预算。
139
- const bodyCost = Math.min(lineNos.length, maxPerFile) * (1 + 2 * context);
140
- const totalCost = lineNos.length + bodyCost;
141
- if (hits.length >= MAX_RESULTS || totalCost > MAX_RESULTS * 4) {
142
- // 文件过多 / 配额爆:仅追加该文件行号列表,不再展开 body
143
- if (hits.length < MAX_RESULTS) {
144
- hits.push({ path: f, lineNos, bodies: [] });
196
+ let timedOut = false;
197
+ const timer = setTimeout(() => {
198
+ timedOut = true;
199
+ controller.abort();
200
+ }, GREP_TIMEOUT_MS);
201
+ try {
202
+ const result = await runRipgrep({ pattern, glob, context }, controller.signal);
203
+ switch (result.status) {
204
+ case 'regex_error':
205
+ return `错误:非法正则 ${pattern}: ${result.message}`;
206
+ case 'no-match': {
207
+ const trunc = result.truncated ? '\n...(输出截断,缩小 glob 或收窄正则后重试)' : '';
208
+ return `无匹配(扫描了 ${result.scanned} 个文件)${trunc}`;
145
209
  }
146
- truncated = true;
147
- continue;
210
+ case 'ok':
211
+ return renderResults(result.files, maxPerFile, context, result.truncated);
212
+ case 'unavailable':
213
+ return nodeFallback(pattern, glob, maxPerFile, context, controller.signal, external);
214
+ case 'aborted':
215
+ if (timedOut) {
216
+ return {
217
+ status: 'error',
218
+ code: 'TIMEOUT',
219
+ retryable: false,
220
+ output: `错误:grep 超过 ${GREP_TIMEOUT_MS / 1000}s 超时墙已被中止(收窄 glob 或确认无网络盘挂载后重试)`,
221
+ };
222
+ }
223
+ // 外部 Ctrl+C:返回中断串,tool-runtime 见 signal.aborted 会归一为 aborted。
224
+ return '已中断';
148
225
  }
149
- hits.push({ path: f, lineNos, bodies: renderBodies(lines, lineNos, maxPerFile, context) });
150
226
  }
151
- const skippedNote = skippedTooLarge
152
- ? `,跳过 ${skippedTooLarge} 个超过 ${MAX_FILE_BYTES / 1024 / 1024}MiB 的文件`
153
- : '';
154
- if (hits.length === 0)
155
- return `无匹配(扫描了 ${scanned} 个文件${skippedNote})`;
156
- const out = [];
157
- let totalShown = 0;
158
- for (const h of hits) {
159
- out.push(`${h.path}: ${h.lineNos.length} 处匹配,行号 [${h.lineNos.join(', ')}]`);
160
- if (h.bodies.length > 0) {
161
- out.push(...h.bodies);
162
- }
163
- else {
164
- out.push(` (body 已折叠,见上方行号列表 → read_file 精读)`);
165
- }
166
- totalShown += h.lineNos.length + h.bodies.length;
167
- if (totalShown >= MAX_RESULTS) {
168
- out.push(`...(结果达到 ${MAX_RESULTS} 条上限${context > 0 ? ',context 行同样计入;要更全就减小 context 或收窄 glob' : ''})`);
169
- break;
170
- }
227
+ finally {
228
+ clearTimeout(timer);
229
+ external?.removeEventListener('abort', onExternal);
171
230
  }
172
- if (truncated)
173
- out.push(`...(仍有更多匹配文件未展示,缩小 glob 或收窄正则)`);
174
- if (skippedTooLarge)
175
- out.push(`...(跳过 ${skippedTooLarge} 个超过 ${MAX_FILE_BYTES / 1024 / 1024}MiB 的文件)`);
176
- return out.join('\n');
177
231
  },
178
232
  };
@@ -52,8 +52,8 @@ 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 },
56
- grep: { effect: 'read', concurrency: 'parallel', resources: workspaceResource },
55
+ glob: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
56
+ grep: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
57
57
  // 网络只读查询:无副作用,可安全重复执行 → idempotent 让 runtime 对瞬时失败(429/5xx/超时/
58
58
  // 网络抖动)自动退避重试,省掉模型「再发一轮 tool call 自救」的往返。写/进程类工具绝不置此位。
59
59
  web_search: { effect: 'network', concurrency: 'parallel', supportsAbort: true, idempotent: true },
@@ -90,6 +90,13 @@ export const CAPABILITIES = {
90
90
  parallelOrchestration: true,
91
91
  supportsAbort: true,
92
92
  },
93
+ // arena:一次调用内部并行 spawnAgent 出多个候选 + 一次 judge(见 ./arena.ts)。与 sub-agent 同理:
94
+ // 资源锁委派给内层 spawn 出的工具,否则 arena 自持 workspace 写锁会与候选内 run_command 自锁;
95
+ // 候选直接写共享工作区 → effect=write;它是单个工具调用,不置 parallelOrchestration(那是跨调用批)。
96
+ arena: { effect: 'write', concurrency: 'serial', delegatesResourceLocks: true, supportsAbort: true },
97
+ // message_bus:读写 .mocode/bus 下的内部消息文件(send 写 / inbox·ack·history 多为读),不触碰
98
+ // 项目代码;用独立 message-bus 资源键串行(类比 memory-store),不作项目 mutation 追踪/回滚。
99
+ message_bus: { effect: 'write', concurrency: 'serial', resources: () => ['message-bus'] },
93
100
  };
94
101
  const rawBuiltinTools = [
95
102
  readFileTool,
@@ -88,6 +88,17 @@ export const IGNORE = [
88
88
  '**/rust/**',
89
89
  '**/dist/**',
90
90
  '**/dist-tests/**',
91
+ // Python 生态:虚拟环境与缓存目录动辄上万文件,ripgrep 路径下主要靠 .gitignore 自动跳过,
92
+ // 这里是「非 git 仓库 / 未写进 .gitignore」时的兜底;Node 兜底路径更依赖这份硬名单。
93
+ '**/.venv/**',
94
+ '**/venv/**',
95
+ '**/__pycache__/**',
96
+ '**/.mypy_cache/**',
97
+ '**/.pytest_cache/**',
98
+ '**/.tox/**',
99
+ '**/htmlcov/**',
100
+ '**/.ipynb_checkpoints/**',
101
+ '**/*.egg-info/**',
91
102
  ];
92
103
  // ── legacy profile fallback(官方 TUI/stdio 已由 ToolPolicy 控制)────────────────
93
104
  // 保留导出名以兼容未传 ToolPolicy 的嵌入调用与旧测试。
@@ -0,0 +1,180 @@
1
+ // ripgrep 子进程客户端:grep 工具的快速路径。
2
+ //
3
+ // 为什么不是 Node 逐文件读:fast-glob 枚举 + readFile 全量进内存 + 逐行正则是串行的,
4
+ // .venv / node_modules 这类目录能把一次全库 grep 拖到 7 分钟以上(Windows Defender 还会
5
+ // 对每个被读文件再扫一遍)。ripgrep 是流式并行目录遍历,默认尊重 .gitignore、跳过二进制
6
+ // 与隐藏目录,速度快一到两个数量级。二进制随 @vscode/ripgrep 按平台安装,不依赖用户 PATH。
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
9
+ import { createRequire } from 'node:module';
10
+ import { IGNORE } from './constants.js';
11
+ import { getSandboxRoot, isInsideRoot } from '../sandbox/index.js';
12
+ const require = createRequire(import.meta.url);
13
+ const isWin = process.platform === 'win32';
14
+ /** JSON 采集上限:约可容纳数万条命中,超出即杀进程并标 truncated。 */
15
+ const MAX_JSON_BYTES = 8 * 1024 * 1024;
16
+ /**
17
+ * 定位 rg 可执行文件:MOCODE_RG_PATH 显式覆盖 → @vscode/ripgrep 随包二进制 → PATH 上的 rg。
18
+ * 返 null = 三条路都没有,调用方应退 Node 兜底。
19
+ */
20
+ export function resolveRgPath() {
21
+ const override = process.env.MOCODE_RG_PATH;
22
+ if (override && existsSync(override))
23
+ return override;
24
+ try {
25
+ const mod = require('@vscode/ripgrep');
26
+ if (typeof mod.rgPath === 'string' && existsSync(mod.rgPath))
27
+ return mod.rgPath;
28
+ }
29
+ catch {
30
+ // 可选依赖没装上(如 --no-optional / 不支持的平台),落到 PATH 探测。
31
+ }
32
+ const probe = spawnSync(isWin ? 'rg.exe' : 'rg', ['--version'], { stdio: 'ignore', windowsHide: true });
33
+ return probe.status === 0 ? (isWin ? 'rg.exe' : 'rg') : null;
34
+ }
35
+ function killTree(child) {
36
+ try {
37
+ if (isWin && child.pid != null) {
38
+ spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
39
+ }
40
+ else {
41
+ child.kill('SIGTERM');
42
+ }
43
+ }
44
+ catch {
45
+ // 进程已退出或终止失败,尽力而为。
46
+ }
47
+ }
48
+ /** 去掉 rg 路径前缀 ./ 并把反斜杠归一为 /(与旧 fast-glob 输出口径一致)。 */
49
+ function normalizePath(p) {
50
+ return p.replace(/^\.[\\/]/, '').replace(/\\/g, '/');
51
+ }
52
+ /**
53
+ * 跑一次 ripgrep(--json NDJSON),聚合成每文件事件。永不抛错:
54
+ * 进程起不来 → unavailable;非法正则 → regex_error;signal 中止 → aborted。
55
+ */
56
+ export function runRipgrep(params, signal) {
57
+ const rgPath = resolveRgPath();
58
+ if (!rgPath || signal?.aborted)
59
+ return Promise.resolve({ status: 'aborted' });
60
+ const cwd = getSandboxRoot() ?? process.cwd();
61
+ // --no-require-git:rg 默认只在 git 仓库内才读 .gitignore;加上后非 git 项目的 .gitignore
62
+ // 同样生效(实测不加时 ignored/ 会被扫到)。
63
+ const args = ['--json', '--hidden', '--no-require-git', '--path-separator', '/'];
64
+ if (params.context > 0)
65
+ args.push('-C', String(params.context));
66
+ if (params.glob && params.glob !== '**/*')
67
+ args.push('-g', params.glob);
68
+ for (const entry of IGNORE)
69
+ args.push('-g', `!${entry}`);
70
+ args.push('-e', params.pattern);
71
+ return new Promise((resolve) => {
72
+ const child = spawn(rgPath, args, {
73
+ cwd,
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ windowsHide: true,
76
+ });
77
+ let settled = false;
78
+ let collected = 0;
79
+ let truncated = false;
80
+ let stderr = '';
81
+ let buffer = '';
82
+ const byPath = new Map();
83
+ let scanned = 0;
84
+ const finish = (result) => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ signal?.removeEventListener('abort', onAbort);
89
+ resolve(result);
90
+ };
91
+ const onAbort = () => {
92
+ killTree(child);
93
+ finish({ status: 'aborted' });
94
+ };
95
+ const handleLine = (line) => {
96
+ let evt;
97
+ try {
98
+ evt = JSON.parse(line);
99
+ }
100
+ catch {
101
+ return; // 末行被截断,忽略
102
+ }
103
+ const data = evt.data;
104
+ if (!data)
105
+ return;
106
+ if (evt.type === 'end') {
107
+ scanned++;
108
+ return;
109
+ }
110
+ if (evt.type !== 'match' && evt.type !== 'context')
111
+ return;
112
+ const pathText = data.path?.text;
113
+ const lineNo = data.line_number;
114
+ const linesText = data.lines?.text;
115
+ if (typeof pathText !== 'string' || typeof lineNo !== 'number' || typeof linesText !== 'string')
116
+ return;
117
+ const path = normalizePath(pathText);
118
+ if (!isInsideRoot(path))
119
+ return; // 后置兜底:仅留牢内
120
+ let bucket = byPath.get(path);
121
+ if (!bucket) {
122
+ bucket = { path, matches: [], contexts: [] };
123
+ byPath.set(path, bucket);
124
+ }
125
+ const event = { line: lineNo, text: linesText.replace(/\r?\n$/, '') };
126
+ if (evt.type === 'match')
127
+ bucket.matches.push(event);
128
+ else
129
+ bucket.contexts.push(event);
130
+ };
131
+ child.stdout.on('data', (chunk) => {
132
+ if (settled)
133
+ return;
134
+ collected += chunk.length;
135
+ if (collected > MAX_JSON_BYTES && !truncated) {
136
+ truncated = true;
137
+ killTree(child); // 输出已超预算,没必要继续
138
+ return;
139
+ }
140
+ buffer += chunk.toString('utf8');
141
+ let idx;
142
+ while ((idx = buffer.indexOf('\n')) >= 0) {
143
+ const line = buffer.slice(0, idx);
144
+ buffer = buffer.slice(idx + 1);
145
+ if (line)
146
+ handleLine(line);
147
+ }
148
+ });
149
+ child.stderr.on('data', (chunk) => {
150
+ stderr += chunk.toString('utf8');
151
+ if (stderr.length > 4000)
152
+ stderr = stderr.slice(-4000);
153
+ });
154
+ child.on('error', (error) => {
155
+ // ENOENT / EACCES / exec 格式错误 = 二进制不可用,交给 Node 兜底。
156
+ const code = error.code;
157
+ finish({ status: code === 'ENOENT' || code === 'EACCES' ? 'unavailable' : 'aborted' });
158
+ });
159
+ child.on('close', (code) => {
160
+ if (buffer.trim())
161
+ handleLine(buffer.trim());
162
+ // exit 2 + stderr = 正则/参数错误;无命中事件时按 regex_error 上报。
163
+ if (code === 2 && byPath.size === 0 && stderr.trim()) {
164
+ finish({ status: 'regex_error', message: stderr.trim() });
165
+ return;
166
+ }
167
+ if (byPath.size === 0) {
168
+ finish({ status: 'no-match', scanned, truncated });
169
+ return;
170
+ }
171
+ finish({ status: 'ok', files: [...byPath.values()], scanned, truncated });
172
+ });
173
+ if (signal) {
174
+ if (signal.aborted)
175
+ onAbort();
176
+ else
177
+ signal.addEventListener('abort', onAbort, { once: true });
178
+ }
179
+ });
180
+ }
@@ -45,7 +45,6 @@ export async function promptFuzzyPicker(opts) {
45
45
  let cursor = query.length;
46
46
  let matches = [];
47
47
  let selected = 0; // 在 matches 中的下标(可能落在禁用项,移动时跳过)
48
- let menuTop = 0;
49
48
  let resolved = false;
50
49
  let resolve;
51
50
  /** 可选项(非禁用)下标集合。 */
@@ -83,9 +82,8 @@ export async function promptFuzzyPicker(opts) {
83
82
  // 开窗:以 selected 为中心。
84
83
  const visibleCount = Math.min(maxVisible, en.length);
85
84
  const selPos = en.indexOf(selected);
86
- let page = Math.floor(selPos / visibleCount);
85
+ const page = Math.floor(selPos / visibleCount);
87
86
  const winEn = en.slice(page * visibleCount, page * visibleCount + visibleCount);
88
- menuTop = winEn[0];
89
87
  const hasMore = page * visibleCount + visibleCount < en.length;
90
88
  const hasAbove = page > 0;
91
89
  winEn.forEach((idx, i) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.6.6",
4
- "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
3
+ "version": "1.6.7",
4
+ "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mocode": "bin/mocode.js",
@@ -44,6 +44,7 @@
44
44
  "prepare": "node -e \"try{require('child_process').execFileSync('git',['config','core.hooksPath','.githooks'],{stdio:'ignore'})}catch(e){}\""
45
45
  },
46
46
  "dependencies": {
47
+ "@vscode/ripgrep": "^1.18.0",
47
48
  "ajv": "8.20.0",
48
49
  "cli-highlight": "^2.1.11",
49
50
  "dotenv": "^16.0.0",