mingdao-harness 0.6.14 → 0.6.16
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.en.md +5 -4
- package/SECURITY.md +25 -12
- package/docs/CONFIG.md +107 -0
- package/package.json +1 -1
- package/src/agent.js +99 -21
- package/src/cli.js +10 -1
- package/src/config.js +76 -10
- package/src/cost-guard.js +1 -1
- package/src/help.js +1 -1
- package/src/ledger.js +22 -0
- package/src/model-discovery.js +177 -8
- package/src/models.js +250 -93
- package/src/providers/index.js +282 -25
- package/src/tools-flow.js +92 -6
- package/src/web/app.js +295 -69
- package/src/web/attachments.js +1 -1
- package/src/web/index.html +60 -11
- package/src/web/routes/domains/config.js +46 -8
- package/src/web/server.js +59 -1
package/README.en.md
CHANGED
|
@@ -19,7 +19,7 @@ does not cover (this file is written so a reviewer can decide that, not to make
|
|
|
19
19
|
than it is).
|
|
20
20
|
|
|
21
21
|
- Node.js **>= 18.17** (`package.json` `engines`), no build step, no npm runtime dependencies.
|
|
22
|
-
- Current version: **0.6.
|
|
22
|
+
- Current version: **0.6.14** · License: **MIT**.
|
|
23
23
|
- Repo: <https://github.com/MingDaoTCM/MingDao-Harness> · Package: `mingdao-harness` on npm.
|
|
24
24
|
|
|
25
25
|
## Quickstart (3 steps)
|
|
@@ -66,11 +66,12 @@ enforced by the kernel is described as a declaration, not as a control.
|
|
|
66
66
|
| **Sandbox** — `config.sandbox` | `"off"` (default), `"readonly"` (whole filesystem read-only, `/tmp` writable, network available), `"safe"` (read-only + no network via `unshare-net`; working dir and `/tmp` writable). Implemented with **bubblewrap** on **Linux only**. | If the platform is not Linux or `bwrap` is missing/incapable, it **downgrades to `off`** and says so in the tool result — it never silently pretends to be sandboxed. It wraps the `bash` tool only; the file tools are governed by the permission modes, not by this. |
|
|
67
67
|
| **Credential separation** | API keys live in `<home>/credentials.json` (`<home>` defaults to `~/.mingdao`, overridable with `MINGDAO_HOME`), written atomically and forced to mode **600**. `config.json` is key-free, so it is shareable and committable. Resolution order: environment variable → local credential store → legacy `config.json` field. Manage with `mingdao key`. | The legacy `config.json` `"apiKey"` field is still **read** for backward compatibility — do not use it. Keys are stored as plaintext in your home directory (no OS keychain), so any process running as your user can read them. `mingdao key status` and logs only ever print a masked form. |
|
|
68
68
|
| **Telemetry opt-out** | Only **packaged desktop builds** send anything, and only one event: after an update finishes downloading, a `POST` to `https://harness.mingdao.ai/updok` with `{"kind":"update","os":...,"ver":...}` — no user identifier, no session, no code. | It is **opt-out, not opt-in**: on by default in packaged builds. Disable with `MINGDAO_NO_TELEMETRY=1` or `"telemetry": false` in `config.json`; updates still download and install. The CLI and the WebUI send **no** telemetry at all. |
|
|
69
|
+
| **Update-package signature** | The packaged desktop app verifies an **ed25519 detached signature** over the installer bytes against a public key pinned in the source (`desktop/update-verify.js`). A signature that is present but invalid is always rejected. | The release pipeline does not sign builds yet, so current builds verify as "no signature" and follow the default policy: **install, but state that the signature was not verified**. Set `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` to **reject** unsigned updates (fail-closed, recommended for high-assurance deployments). A feed controlled by an attacker can strip the signature — that is exactly the downgrade this switch closes off. |
|
|
69
70
|
| **PID ownership** | Before `killTask` / `stopDaemon` signals a pid, it verifies the pid still belongs to the process we started (Linux reads `/proc/<pid>/cmdline`, macOS falls back to `ps -ww -o command=`). Three-valued: ours / definitely not ours / unknown. | **Windows has neither `/proc` nor `ps`**, and the project deliberately does not take a PowerShell/WMI dependency for this check, so ownership is reported as unknown and the caller falls back to best-effort "process is alive". Deploy on Linux or macOS if you need precise pid-ownership checks. |
|
|
70
71
|
| **Egress gate** — `config.net` | `{ "allow": ["api.deepseek.com", "*.internal.corp", "10.0.0.0/8"], "mode": "warn", "allowLoopback": true }` — `warn` (default) logs and allows, `block` refuses. Decisions are made at a single HTTP exit and recorded per event (host, port, decision, matched rule); `mingdao net report` exports the record as evidence. Non-`fetch` paths (`node:https` sync, `safe-fetch` for skill/registry/model discovery) are wired into the same decision point. Loopback is exempt; private ranges are not. | **It does not cover child processes.** Your own `curl` inside `bash`, **MCP server processes**, `config.tools` and Pack-spawned subprocesses, and the Electron shell's update check all make their own requests that the gate never sees. It proves *"the kernel did not exfiltrate"*, **not** *"this machine never egressed"*. `net.jsonl` records hosts/ports/decisions, never request bodies or full URLs. If unconfigured, the gate is not installed at all. For process-level enforcement use an egress proxy or firewall. |
|
|
71
72
|
| **SSRF protection** | One implementation (`src/ssrf-guard.js`): literal private / loopback / link-local / cloud-metadata hosts are refused (IPv4 plus all IPv6 forms, including IPv4-mapped, NAT64 and zone-ids); a hostname is resolved and **every** resolved address is checked; a hostname that fails to resolve is refused. The **validated IP is pinned to the connection layer** (via the `lookup` option of `node:http(s)`), so the check-time and connect-time resolutions cannot differ — this closes DNS rebinding. Redirects are re-checked per hop and cross-origin hops drop credential headers using a reverse allowlist. | The rule is fail-closed by design, which means it can over-block unusual-but-legitimate targets (odd IPv6 forms, `%`-scoped hosts); the fix is to use a literal, routable address or a proper hostname. It protects harness-initiated requests; it cannot constrain a subprocess' own networking (see the row above). |
|
|
72
73
|
| **Pack trust** | A project-level `.mingdao/packs/**` is **not mounted** until you explicitly run `mingdao pack trust <project-dir>`. Trust is pinned to content: if the Pack's content changes afterwards, trust lapses automatically. `mingdao pack verify <dir>` is **static by default** and does not execute Pack code; only `--runtime` imports it. | `manifest.permissions` (`fs` / `net` / `env`) is a **declaration, not enforcement**. The kernel does not enforce it: `pack.mjs` is imported into the **host process** and runs with full Node privileges — it can read and write any file and reach the network — and Pack-contributed tools are **not** subject to `config.permission`. Packs you list explicitly in `config.packs` bypass the trust gate, because that is your own written authorization. Treat installing a Pack as equivalent to running its code. |
|
|
73
|
-
| **Execution ledger** | Per-turn event streams under `<home>/ledger/`, each event chained to the previous one by a truncated SHA-256 digest starting from a fixed genesis value, plus a `run.end` seal sidecar. Details are redacted on write and again on export. Inspect with `mingdao ledger list\|show\|export\|verify` (
|
|
74
|
+
| **Execution ledger** | Per-turn event streams under `<home>/ledger/`, each event chained to the previous one by a truncated SHA-256 digest starting from a fixed genesis value, plus a `run.end` seal sidecar. The seal carries an **ed25519 source signature** (key at `<home>/ledger-key.json`, mode 600, generated on first seal). Details are redacted on write and again on export. Inspect with `mingdao ledger list\|show\|export\|verify` — `verify` reports chain/seal **and** provenance in four states (valid / unsigned / invalid / unverifiable) — plus `replay`, which re-evaluates recorded calls against your *current* rules offline; `ledger --sign-key` shows or rotates the key. | Hash chain + seal prove "unchanged and not truncated **since it was written**"; the signature proves "written by the holder of that key" — but the key lives on the same machine with the same permissions, so it does **not** stop a local attacker who can already rewrite the ledger. There is **no trusted timestamp**, and replacing the key makes old ledgers report "signed by another key". It is provenance, not audit-grade non-repudiation. |
|
|
74
75
|
| **WebUI exposure** | Binds `127.0.0.1:3820` by default. When bound to a non-loopback address without a configured token, the server generates a random token for that run and prints a `?token=` URL. Tokens also accepted via `X-MingDao-Token` or `Authorization: Bearer`. Host header is checked against loopback/bind names (anti-DNS-rebinding), cross-site browser requests are rejected, and cloud-metadata endpoints are always refused. | On loopback with no token configured there is **no authentication** (local trust by design). With `host: 0.0.0.0` the Host-header check is relaxed and the **token becomes the only boundary**, so a token is mandatory in that mode. The token is deployment-level: everyone sharing it is the same user, and task/session lists are mutually visible. |
|
|
75
76
|
|
|
76
77
|
## Command index
|
|
@@ -87,7 +88,7 @@ Commands are grouped here for evaluation; `mingdao --help` and `src/help.js` are
|
|
|
87
88
|
| `mingdao web [port]` | Start the WebUI (default `http://127.0.0.1:3820`); `--auth-token` (or `--auth-token=-` from stdin). |
|
|
88
89
|
| `mingdao diagnose` | Build one redacted diagnostic bundle (logs / audit / config) for bug reports. |
|
|
89
90
|
| `mingdao audit [n]` | Show the tool-call audit log (default last 20 entries). |
|
|
90
|
-
| `mingdao ledger list\|show\|export\|verify\|replay` | Execution ledger: inspect, export, verify the hash chain, or replay against current rules. |
|
|
91
|
+
| `mingdao ledger list\|show\|export\|verify\|replay` | Execution ledger: inspect, export, verify the hash chain and source signature, or replay against current rules. `ledger --sign-key [--generate\|--force]` shows or rotates the ed25519 signing key. |
|
|
91
92
|
| `mingdao net report\|policy` | Egress record export and current `config.net` policy. |
|
|
92
93
|
| `mingdao key [status\|set\|remove\|import] [provider]` | Manage the local credential store (never writes keys into `config.json`). |
|
|
93
94
|
| `mingdao pack list\|verify\|info\|new\|trust\|untrust` | Discover, statically validate, scaffold, and trust domain Packs. |
|
|
@@ -111,7 +112,7 @@ Commands are grouped here for evaluation; `mingdao --help` and `src/help.js` are
|
|
|
111
112
|
The full reference is **Chinese**; these are the entry points worth knowing:
|
|
112
113
|
|
|
113
114
|
- [`docs/CONFIG.md`](docs/CONFIG.md) — every `config.json` field: permission rules, sandbox, `config.net`, hooks, MCP, sync, cost guard.
|
|
114
|
-
- [`docs/CONFIG.en.md`](docs/CONFIG.en.md) — English configuration reference (verified against the code): permissions, sandbox & env filtering, egress gate and its limits, cost guard, WebUI exposure, audit/ledger, full field table.
|
|
115
|
+
- [`docs/CONFIG.en.md`](docs/CONFIG.en.md) — English configuration reference (verified against the code): permissions, sandbox & env filtering, egress gate and its limits, cost guard, WebUI exposure, audit/ledger provenance, local-endpoint pre-flight/serialization, timeouts and the turn-level watchdog, full field table.
|
|
115
116
|
- [`docs/PACK-API.md`](docs/PACK-API.md) — the Pack contract (manifest, contributions, constraint kinds, and which of them are **not** implemented).
|
|
116
117
|
- [`docs/PROVIDERS.md`](docs/PROVIDERS.md) — built-in providers and writing a custom OpenAI-compatible provider.
|
|
117
118
|
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — module map and data flow.
|
package/SECURITY.md
CHANGED
|
@@ -11,7 +11,7 @@ Security fixes are provided for the **latest minor release only**.
|
|
|
11
11
|
|
|
12
12
|
| Version | Supported |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| **0.6.x** (current: **0.6.
|
|
14
|
+
| **0.6.x** (current: **0.6.14**) | Yes |
|
|
15
15
|
| 0.5.x and older | No |
|
|
16
16
|
|
|
17
17
|
Upgrade with `npm i -g mingdao-harness@latest` (npm installs), or `mingdao update` / `mingdao rollback`
|
|
@@ -48,7 +48,8 @@ Please do not open a public issue for an unfixed vulnerability. Credit is offere
|
|
|
48
48
|
- `src/permissions.js` — the permission decision engine: `allow` / `deny` / `denyStrict` matching and
|
|
49
49
|
the `readonly` / `ask` / `auto` modes.
|
|
50
50
|
- `desktop/**` — the Electron shell: window navigation and `openExternal` handling, the contextBridge
|
|
51
|
-
preload surface, IPC handlers
|
|
51
|
+
preload surface, IPC handlers, and update-package signature verification (a bypass of the pinned key or
|
|
52
|
+
of the fail-closed policy is in scope).
|
|
52
53
|
|
|
53
54
|
Reports that affect anything else in `src/` are welcome too; the list above is where the trust
|
|
54
55
|
boundaries live.
|
|
@@ -90,14 +91,18 @@ boundaries live.
|
|
|
90
91
|
5. **Treat Packs as untrusted code.** Run `mingdao pack verify <dir>` (static; `--runtime` executes the
|
|
91
92
|
Pack) and read `pack.mjs` before `mingdao pack trust`; never trust a freshly cloned repository you
|
|
92
93
|
have not reviewed.
|
|
93
|
-
6. **Enable the audit trail and the ledger.** Keep `config.audit` on,
|
|
94
|
-
periodically, and run `mingdao ledger verify` on turns you care about
|
|
94
|
+
6. **Enable the audit trail and the ledger, and keep the signing key.** Keep `config.audit` on,
|
|
95
|
+
review `mingdao audit` periodically, and run `mingdao ledger verify` on turns you care about; a new
|
|
96
|
+
ledger is signed automatically with `<home>/ledger-key.json` (mode 600). Back that file up: replacing
|
|
97
|
+
it turns every previously signed ledger into "signed by another key". For unattended jobs, prefer
|
|
95
98
|
`readonly` or `denyStrict` over `auto`.
|
|
96
99
|
7. **Sandbox shell execution where you can.** On Linux with `bubblewrap` installed, set
|
|
97
100
|
`config.sandbox` to `readonly` or `safe` (no network). Elsewhere the sandbox downgrades to `off` and
|
|
98
101
|
says so — compensate with OS-level isolation or a container.
|
|
99
102
|
8. **Run the desktop build only from a source you trust** and verify the checksum published on the
|
|
100
|
-
Release page; current desktop builds are unsigned
|
|
103
|
+
Release page; current desktop builds are unsigned. If you require update provenance, set
|
|
104
|
+
`MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` so an update package without a valid signature is **rejected**
|
|
105
|
+
(the default is to install but explicitly report that the signature was not verified).
|
|
101
106
|
|
|
102
107
|
## Known boundaries
|
|
103
108
|
|
|
@@ -114,15 +119,23 @@ price them in.
|
|
|
114
119
|
2. **Packs have no isolation.** `pack.mjs` runs in the host process with full Node privileges, and
|
|
115
120
|
`manifest.permissions` is not enforced by the kernel. Capability brokering or subprocess isolation
|
|
116
121
|
for Packs requires an architectural change and is not implemented.
|
|
117
|
-
3. **Electron code signing
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
3. **Electron code signing / notarization is still not done.** Current builds are unsigned (see
|
|
123
|
+
[`docs/CODE-SIGNING.md`](docs/CODE-SIGNING.md), which describes the steps to sign). Update-package
|
|
124
|
+
**source-signature verification is implemented** (`desktop/update-verify.js`: ed25519 over the
|
|
125
|
+
installer bytes, public key pinned in the source; a present-but-invalid signature is always rejected),
|
|
126
|
+
but the release pipeline does not sign builds yet. Consequently the default policy is
|
|
127
|
+
`warn-and-install` (install, and state that the signature was not verified); set
|
|
128
|
+
`MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` to make unsigned updates fail closed.
|
|
120
129
|
4. **IDE plugin token storage needs hardening.** The VS Code and JetBrains plugins drive the local
|
|
121
130
|
WebUI over loopback; the broader item (secure token storage, and distinguishing HTTP 401 from
|
|
122
131
|
"server unreachable") is registered as open work.
|
|
123
|
-
5. **Ledger
|
|
124
|
-
modification and truncation after the fact,
|
|
125
|
-
|
|
126
|
-
`mingdao ledger verify`
|
|
132
|
+
5. **Ledger provenance has the same limits as the machine it runs on.** The hash chain plus the
|
|
133
|
+
`run.end` seal detect modification and truncation after the fact, and since v0.6.11 the seal also
|
|
134
|
+
carries an **ed25519 source signature** (auto-generated key at `<home>/ledger-key.json`, mode 600;
|
|
135
|
+
`mingdao ledger --sign-key` to inspect/rotate; `verify` reports valid / unsigned / invalid /
|
|
136
|
+
unverifiable, with "unverifiable" also failing). The boundary: the private key and the ledger share
|
|
137
|
+
the same machine and permissions, so this does not defend against a local attacker who can already
|
|
138
|
+
rewrite the ledger; there is no trusted timestamp; and replacing the key makes old ledgers report
|
|
139
|
+
"signed by another key". It is provenance, not audit-grade non-repudiation.
|
|
127
140
|
|
|
128
141
|
We do not claim this project is vulnerability-free, and this document is not a marketing statement.
|
package/docs/CONFIG.md
CHANGED
|
@@ -373,6 +373,56 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
373
373
|
|
|
374
374
|
长期偏好仍走用户记忆(`AGENTS.md`,见上),不受此开关影响。
|
|
375
375
|
|
|
376
|
+
## 模型名称以 API 为准(v0.6.19,§3.67)
|
|
377
|
+
|
|
378
|
+
**仓库不维护具体版本号,只维护「家族能力 / 定价模式」。** 官方改名、升版本(例如
|
|
379
|
+
`deepseek-v4-flash` → `deepseek-flash` → `DeepSeek-V4.1-Flash 1017` → 未来的 V4.2/V5)
|
|
380
|
+
**不需要改代码**:模型名一律以 `GET /v1/models` 实时返回的官方名为准。
|
|
381
|
+
|
|
382
|
+
### 家族模式表
|
|
383
|
+
|
|
384
|
+
`src/models.js` 的 `MODEL_FAMILIES` 用正则匹配名字,能力(上下文窗口、单次输出上限、工具调用、
|
|
385
|
+
视觉)与定价挂在**模式**上:
|
|
386
|
+
|
|
387
|
+
| 家族 | 匹配(大小写 / 空格 / 连字符 / 版本号无关) | 典型官方名 |
|
|
388
|
+
| --- | --- | --- |
|
|
389
|
+
| `pro` | `deepseek` + 可选版本 + `pro` | `DeepSeek-V4.1-Pro 1017` |
|
|
390
|
+
| `flash` | `deepseek` + 可选版本 + `flash` | `DeepSeek-V4.1-Flash 1017` |
|
|
391
|
+
| `vision` | `deepseek` + 可选版本 + `flash` + `vision` | `DeepSeek-V4.1-Flash-Vision 1017` |
|
|
392
|
+
|
|
393
|
+
- 精确 id 只作为**已知别名**存在(如旧名 `deepseek-v4-flash`),老配置与历史会话照常可用;
|
|
394
|
+
路由比较前会归一化到家族代表名,不会因为厂家改名而静默失效。
|
|
395
|
+
- 比已核对规格更新的版本(例如 `DeepSeek-V4.2-Flash 1101`)**仍可选中**,列表会标
|
|
396
|
+
「能力未知(新版本)」并按家族规格保守估算上下文与价格;需要精确规格时用
|
|
397
|
+
`customModels.<模型名>.contextWindow` / `config.pricing.overrides` 覆盖。
|
|
398
|
+
|
|
399
|
+
### 默认值如何解析
|
|
400
|
+
|
|
401
|
+
默认模型(新建配置、WebUI 模型下拉、自动路由的 planner / executor)在**运行时**按下列顺序解析:
|
|
402
|
+
|
|
403
|
+
1. 用户显式配置优先:`config.model`、`config.routing.planner`、`config.routing.executor`;
|
|
404
|
+
2. 否则从**当前 API 名单**里按家族挑官方名:planner → `pro` 家族,executor → `flash` 家族,
|
|
405
|
+
多个版本并存时取最高版本;
|
|
406
|
+
3. 名单里没有对应家族、或 API 不可达时,才退回**内置家族代表名**,并输出提示:
|
|
407
|
+
- 服务端日志:`[MingDao] ⚠ … 默认模型走兜底 …`
|
|
408
|
+
- WebUI / HTTP:`GET /api/models` 与 `GET /api/state` 的 `note` 字段,离线时
|
|
409
|
+
`offlineFallback: true`,文案为「离线兜底:模型名可能不是最新的」。
|
|
410
|
+
|
|
411
|
+
离线时模型列表使用内置家族代表名(不是最新官方版本),并在每个条目上标
|
|
412
|
+
「离线兜底」;连上 API 后刷新即恢复官方名。
|
|
413
|
+
|
|
414
|
+
**自定义端点不主动探测。** `provider: "custom"`(以及未内置的服务商)不会收到
|
|
415
|
+
`GET /models` 请求——很多自定义网关只实现 chat,空 body 的 `/models` 请求会让它们直接崩。
|
|
416
|
+
这类端点的默认 planner / executor 用家族代表名兜底,需要指定时在 `config.routing.*`
|
|
417
|
+
里写端点自己的模型名。
|
|
418
|
+
|
|
419
|
+
### 旧名与自定义模型
|
|
420
|
+
|
|
421
|
+
- 旧名(`deepseek-v4-flash`、`deepseek-flash`、`deepseek-v4-pro`、`deepseek-v4-flash-vision-exp`)
|
|
422
|
+
仍可作为 `--model` / `config.model` 的取值;能力与定价照旧按家族解析。
|
|
423
|
+
- 完全不属于任何家族的线上新模型(例如非 DeepSeek 命名)仍会出现在列表里,可直接选中;
|
|
424
|
+
能力未知时按远程模型保守默认(上下文 128k)估算,可用 `customModels` 显式覆盖。
|
|
425
|
+
|
|
376
426
|
## 自定义模型(WebUI 添加后落盘的结构)
|
|
377
427
|
|
|
378
428
|
```json
|
|
@@ -531,6 +581,52 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
531
581
|
既有默认, 1800s)、无进展看门狗 = clamp(max(实测首帧×100, 参考prefill×20), 10min, 60min);
|
|
532
582
|
**你显式配置的 `timeout.*` / `noProgressTimeoutMs` 永远优先**(显式 > 自适应)。
|
|
533
583
|
|
|
584
|
+
#### 本地模型:并发与内存(v0.6.15)
|
|
585
|
+
|
|
586
|
+
真机证据(v0.6.14,8081 的 `mtplx-qwen38-27b-optimized-quality`):长任务中途被引擎拒绝——
|
|
587
|
+
`insufficient memory: the request exceeded available GPU memory (sustained critical memory pressure
|
|
588
|
+
during prefill; aborted before the allocator wall). The engine shed its caches and stays up; this
|
|
589
|
+
request failed.` 同一错误此前在 60091 的 35B 上也出现过 → **不是模型问题,是本机内存压力**。
|
|
590
|
+
观测到该回合此前有子代理在跑(13 轮 / 17 次工具调用)→ **父回合 + 子代理同时向同一本地引擎
|
|
591
|
+
发大 prefill**(各自要一份 KV cache)是主要来源。所以内核现在对**同一 `baseUrl`** 的本地端点
|
|
592
|
+
在途请求做**串行化/限量**:
|
|
593
|
+
|
|
594
|
+
- **默认 1 并发**:父回合、子代理(`task`)、**端点预检**(探针打的是同一个 `/chat/completions`)
|
|
595
|
+
共用同一条**进程级 Promise 队列**,谁先到谁先跑,后到的排队(不是忙等,释放即移交名额)。
|
|
596
|
+
- **配置**(与 `timeout` 同级,写在 `config.json` 顶层):
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"localConcurrency": 1,
|
|
601
|
+
"localQueueTimeoutMs": 1800000
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
| 字段 | 默认 | 含义 |
|
|
606
|
+
| --- | --- | --- |
|
|
607
|
+
| `localConcurrency` | `1` | 同一本地 `baseUrl` 的同时在途请求上限。`0` = **显式退出串行化**(不限并发);正整数上限 16;非法/负数回落 1 |
|
|
608
|
+
| `localQueueTimeoutMs` | `1800000`(30 分钟) | 排队等待上限。超过即报**可见错误**(不重试、不死等),避免"看起来在跑、其实永远排不到" |
|
|
609
|
+
|
|
610
|
+
- **云端端点不受影响**:`baseUrl` 不是回环/内网(如 `api.deepseek.com`)时直接放行——不排队、
|
|
611
|
+
不记日志、不增加任何延迟。`customModels.<名>.local: true` 可把经反代的本地端点纳入串行化。
|
|
612
|
+
- **可见性**:入队时写一行 `本地端点串行化:等待 N 个在途请求(端点,并发上限 1)`
|
|
613
|
+
(WebUI 落 `~/.mingdao/logs/web-server.log`;CLI 写 stderr);界面状态条/进度行显示
|
|
614
|
+
`本地端点已串行化(防内存压力)`,发送前的 `📏 本轮上下文` 预告在**已有在途请求**时会加一条强警告
|
|
615
|
+
`并发会加剧内存压力 … 建议减少子代理数量`。
|
|
616
|
+
- **排队超时的报错**(`code=LOCAL_QUEUE_TIMEOUT`):
|
|
617
|
+
`本地端点排队超时(等待 1800.0s,仍有 1 个在途请求,并发上限 1):为避免内存压力已在本机端点串行化——可调大 config.localQueueTimeoutMs,或减少子代理/并行会话。`
|
|
618
|
+
- **引擎活着但丢了缓存**:上游文案含 `engine shed its caches and stays up` / `allocator wall` 时,
|
|
619
|
+
错误分型会明确"**该引擎未崩溃**(自己丢弃缓存后继续运行)"、**不要重启引擎**,处置是
|
|
620
|
+
压上下文(`contextBudget` / `/compact`)、减少并发、等它自行恢复后重试。这与"引擎崩了/连接被拒"
|
|
621
|
+
(`ECONNREFUSED`、连接被断开)是**不同**的两种判定,后者才需要确认端口与进程。
|
|
622
|
+
- **排查步骤**(还遇到内存拒绝时):
|
|
623
|
+
1. 看 `web-server.log` 有没有 `本地端点串行化:等待 N 个在途请求`——有排队说明闸门在工作,
|
|
624
|
+
若仍被拒绝,说明单请求本身就超了引擎内存(压 `contextBudget`,或让 `customModels.<名>.contextWindow`
|
|
625
|
+
≤ 引擎自述的 `n_ctx`,见上一节);
|
|
626
|
+
2. 确认没有多个会话/多个本地模型同时跑长任务(每个模型各占一份显存与 KV cache);
|
|
627
|
+
3. 临时把 `localConcurrency` 保持 `1`,并减少子代理数量(读只读子代理在本地模型下已不并行);
|
|
628
|
+
4. **不要**因为 `engine shed its caches and stays up` 去重启引擎——它没有崩;重启只会重新加载模型。
|
|
629
|
+
|
|
534
630
|
### 用本地模型测 harness(测试者步骤清单)
|
|
535
631
|
|
|
536
632
|
> 这一段是**操作清单**,不是原理说明。按顺序做,能在几分钟内区分"端点不行"和"harness 不行"。
|
|
@@ -653,6 +749,11 @@ mingdao ledger verify <runId> --key <文件> # 用指定公钥验签(本
|
|
|
653
749
|
顺手一次就会把签名密钥清掉、让全部历史账本变成"另一把密钥签发"。
|
|
654
750
|
- **默认开启**:新账本在首次收尾时若本机还没有密钥会**自动生成**并签名(可选签名在合规上等于
|
|
655
751
|
没有签名)。封条里带 `sig{alg,keyId,value}`,`run.end` 链内带 `sigKey`(公钥指纹)。
|
|
752
|
+
- **`run.start.permissionSource`(v0.6.17 §3.64)**:记录本回合生效权限的**来源**——`explicit`
|
|
753
|
+
(用户/调用方显式选择,如 WebUI 权限档)/ `preset`(预设声明生效)/ `config`(`config.json`
|
|
754
|
+
的 `permission`)/ `cli`(后台/调度任务由 CLI 参数指定)/ `default`(以上都没有,落到内置缺省
|
|
755
|
+
`ask`;WebUI 的 `config` 分支在缺字段时也归 `config`)。`permission` 仍是生效值、`LEDGER_VERSION`
|
|
756
|
+
不变;旧账本没有该字段照样 `verify` 通过(`show` 只是少一行可选的「权限来源」)。
|
|
656
757
|
- **`ledger verify` 的三种结论**(前两者退出码 0,第三者非 0):
|
|
657
758
|
|
|
658
759
|
| 结论 | 含义 | 退出码 |
|
|
@@ -671,3 +772,9 @@ mingdao ledger verify <runId> --key <文件> # 用指定公钥验签(本
|
|
|
671
772
|
(按模型分账、每日费用柱状图、缓存命中率、Batch 子项),文件落当前目录
|
|
672
773
|
`mingdao-cost-report-<月>.md`。数据源为 cache-stats.jsonl(含缓存折扣与 Batch 半价的真实口径)。
|
|
673
774
|
|
|
775
|
+
|
|
776
|
+
## 开发工具:变异验证的 fail-safe
|
|
777
|
+
|
|
778
|
+
`node test/mutate/run.mjs` 启动时会先自检残留标记(能自愈的自愈,冲突/在飞则拒绝),
|
|
779
|
+
并用 `test/mutate/.generated/mutate.lock` 做互斥;详见 `test/mutate/FAILSAFE.md`。
|
|
780
|
+
手工自检:`node test/mutate/check.mjs`(`--allow-dirty` 可放行脏工作树)。
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -18,15 +18,16 @@ import { redactSecrets } from './redact.js';
|
|
|
18
18
|
import { checkCostGuard, costGuardConfig, todayCost, preflightBlockMessage, roundGuardAction } from './cost-guard.js';
|
|
19
19
|
import { recordCacheStats, packDailyCost } from './cachestats.js';
|
|
20
20
|
import { estimateCost, cacheSplit, isPeakHour } from './pricing.js';
|
|
21
|
-
import { resolveProviderConfig, createProvider } from './providers/index.js';
|
|
21
|
+
import { resolveProviderConfig, createProvider, localGateSnapshot, localConcurrencyLimit } from './providers/index.js';
|
|
22
22
|
import { compileConstraints, checkPreTool, checkPostTool, checkOutput, blockedOutputText } from './constraints.js';
|
|
23
23
|
import { createLedger, newRunId } from './ledger.js';
|
|
24
24
|
import { registerEgressSink } from './net-guard.js';
|
|
25
25
|
import { getActivePackContext } from './packs.js';
|
|
26
26
|
// v0.6.11(审计 P1-1 拆分第三刀):工具编排两端的判据单源在 tools-flow.js——
|
|
27
27
|
// ① 本轮可见哪些工具(只读档 × 预设白名单 × 已用工具快照 × MCP 只读标注);
|
|
28
|
-
// ② 工具结果怎么回填(序列化 / 复用前缀 / 约束拒绝文案 /
|
|
29
|
-
|
|
28
|
+
// ② 工具结果怎么回填(序列化 / 复用前缀 / 约束拒绝文案 / 按窗口截断);
|
|
29
|
+
// ③ 工具参数怎么解析(v0.6.17 第四刀:JSON + 容错 + 归一化 + 错误文案)。
|
|
30
|
+
import { READONLY_TIER_SET, visibleToolsFor, serializeToolResult, toolResultMessage, parseToolArgs } from './tools-flow.js';
|
|
30
31
|
|
|
31
32
|
const MAX_STEPS = 24;
|
|
32
33
|
// 子代理步数上限:审计/精读类只读子任务需要读多个文件 + 交叉引用,12 步易在「读不全」时被截断
|
|
@@ -144,11 +145,16 @@ const fmtTok = (/** @type {any} */ n) => Number(n || 0).toLocaleString('en-US');
|
|
|
144
145
|
*
|
|
145
146
|
* v0.6.13(问题 1 修法):**有预检实测就必须由它推导**,并在文案里如实标注
|
|
146
147
|
* ①这是外推、②长上下文下外推会低估(附实测差值);只有在**没有预检数据**时才退回保守文案。
|
|
148
|
+
*
|
|
149
|
+
* v0.6.15(§3.59 第 3 条):本地端点**已有在途请求**(父回合/子代理/预检之一)时加一句强警告——
|
|
150
|
+
* 真机 8081 的内存拒绝就是"父子回合同时 prefill"撞出来的;串行化已经兜住资源,但用户仍应
|
|
151
|
+
* 知道"并发会加剧内存压力、应减少子代理"。
|
|
147
152
|
* @param {{ promptTokens: number, contextWindow?: number, isLocal?: boolean,
|
|
148
|
-
* firstTokenMs?: number, budget?: number, probe?: any
|
|
153
|
+
* firstTokenMs?: number, budget?: number, probe?: any,
|
|
154
|
+
* localBusy?: boolean, localActive?: number, localQueued?: number, localConcurrency?: number }} input
|
|
149
155
|
* @returns {{ text: string, warn: boolean, promptTokens: number, ratio: number, estimateMs: number|null }}
|
|
150
156
|
*/
|
|
151
|
-
export function prefillForecast({ promptTokens, contextWindow, isLocal, firstTokenMs, budget, probe = null }) {
|
|
157
|
+
export function prefillForecast({ promptTokens, contextWindow, isLocal, firstTokenMs, budget, probe = null, localBusy = false, localActive = 0, localQueued = 0, localConcurrency = 1 }) {
|
|
152
158
|
const p = Math.max(0, Math.round(Number(promptTokens) || 0));
|
|
153
159
|
const win = Number(contextWindow) > 0 ? Number(contextWindow) : 0;
|
|
154
160
|
const ratio = win > 0 ? p / win : 0;
|
|
@@ -187,10 +193,22 @@ export function prefillForecast({ promptTokens, contextWindow, isLocal, firstTok
|
|
|
187
193
|
`建议缩小任务(新建会话、只给必要文件、先 /compact)或调大 config.timeout.firstTokenMs——` +
|
|
188
194
|
`但注意调大它只会让"失败"更晚被发现,不会让 prefill 变快。`
|
|
189
195
|
: '';
|
|
196
|
+
// v0.6.15(§3.59 第 3 条):本地端点已有在途请求 → **强警告**(不是提示)。
|
|
197
|
+
// 真机证据:父回合 + 子代理同时向同一台 8081 引擎发大 prefill,引擎在 prefill 阶段报
|
|
198
|
+
// insufficient memory("engine shed its caches and stays up");串行化解决资源冲突,
|
|
199
|
+
// 但用户仍必须知道"并发会加剧内存压力",否则会继续开并行子代理/多会话。
|
|
200
|
+
const limit = Number(localConcurrency) >= 0 ? Math.floor(Number(localConcurrency)) : 1;
|
|
201
|
+
const active = Math.max(0, Math.floor(Number(localActive) || 0));
|
|
202
|
+
const queued = Math.max(0, Math.floor(Number(localQueued) || 0));
|
|
203
|
+
const concurrencyNote = Boolean(isLocal) && Boolean(localBusy)
|
|
204
|
+
? `\n ⚠ 本地端点当前已有 ${active} 个在途请求${queued > 0 ? `、${queued} 个在排队` : ''}(并发上限 ${limit}):` +
|
|
205
|
+
`**并发会加剧内存压力**(父子回合同时 prefill 会被引擎以 insufficient memory 拒绝);` +
|
|
206
|
+
`${limit <= 1 ? '已串行化,后来的请求会排队等待' : `已限制为 ${limit} 并发,仍可能叠加内存压力`}——建议减少子代理数量、避免多个长任务会话同时跑。`
|
|
207
|
+
: '';
|
|
190
208
|
// v0.6.13(A 追加):把**端点预检**结果并进这条预告——"这个端点行不行"与"这次要 prefill 多少"
|
|
191
209
|
// 必须一起出现在发送前,而不是等出事后翻日志(引擎实际加载的模型名与配置名不一致也在这里说)。
|
|
192
210
|
const probeLine = probe && probe.note ? `\n 端点预检:${probe.note}` : '';
|
|
193
|
-
return { text: `${head}\n ${timing}${probeLine}${advice}`, warn, promptTokens: p, ratio, estimateMs: est ? est.ms : null };
|
|
211
|
+
return { text: `${head}\n ${timing}${probeLine}${advice}${concurrencyNote}`, warn: warn || Boolean(concurrencyNote), promptTokens: p, ratio, estimateMs: est ? est.ms : null };
|
|
194
212
|
}
|
|
195
213
|
|
|
196
214
|
/**
|
|
@@ -276,6 +294,15 @@ export function describeUpstreamError(/** @type {any} */ err) {
|
|
|
276
294
|
hint =
|
|
277
295
|
'上游拒绝的是**它的 prompt 模板**(不是网络问题):该端点的模板不接受当前 messages 形态(典型:按单轮 query 设计的 chatflow/模板,收到的是 OpenAI 多轮 messages)。' +
|
|
278
296
|
'这类端点通常也**不返回 tool_calls**——请改用支持 function calling 的模型/端点,或让网关把 messages 映射成它要的单轮入参。';
|
|
297
|
+
} else if (/engine shed its caches and stays up|allocator wall/.test(t)) {
|
|
298
|
+
// v0.6.15(§3.59 第 2 条):**引擎活着但丢了缓存**——与"引擎崩了/连接被拒"是两件事。
|
|
299
|
+
// 真机原文(8081):`insufficient memory: … aborted before the allocator wall.
|
|
300
|
+
// The engine shed its caches and stays up; this request failed.` 引擎自己丢了 KV cache
|
|
301
|
+
// 并继续服务,**没有崩溃**;此时建议"重启引擎"是错的(重启只会把模型重新加载一遍)。
|
|
302
|
+
hint =
|
|
303
|
+
'本地引擎**内存/显存不足**,但它**未崩溃**(原文:engine shed its caches and stays up——引擎自己丢弃缓存后仍在运行)。' +
|
|
304
|
+
'**不要重启引擎**(重启不会增加显存,反而要重新加载模型)。处置:压缩上下文(减小 config.contextBudget 或 /compact)、' +
|
|
305
|
+
'减少并发子任务/并行会话(同一本地端点已按 config.localConcurrency 串行化),然后可直接重试——引擎通常会自行恢复。';
|
|
279
306
|
} else if (/memory_refusal|insufficient memory|gib|内存不足|内存拒绝/.test(t)) {
|
|
280
307
|
hint =
|
|
281
308
|
'本地引擎**内存/显存不足**(在 prefill 前就拒绝了)。处置:压缩上下文(减小 config.contextBudget 或 /compact)、减少并发子任务,或重启模型服务释放内存后重试。';
|
|
@@ -302,9 +329,10 @@ export function describeUpstreamError(/** @type {any} */ err) {
|
|
|
302
329
|
* @param {{ phase?: string, elapsedMs: number, modelRounds?: number, toolCalls?: number,
|
|
303
330
|
* toolExecuted?: number, contentChars?: number, ttftMs?: number|null,
|
|
304
331
|
* pendingTool?: { name: string, ms: number }|null, tokensPerSec?: number|null,
|
|
305
|
-
* subagent?: { label?: string, modelRounds?: number, toolCalls?: number, at?: number }|null
|
|
332
|
+
* subagent?: { label?: string, modelRounds?: number, toolCalls?: number, at?: number }|null,
|
|
333
|
+
* localQueue?: { active?: number, queued?: number, limit?: number }|null }} p
|
|
306
334
|
*/
|
|
307
|
-
export function formatTurnProgress({ phase, elapsedMs, modelRounds = 0, toolCalls = 0, toolExecuted = 0, contentChars = 0, ttftMs = null, pendingTool = null, tokensPerSec = null, subagent = null }) {
|
|
335
|
+
export function formatTurnProgress({ phase, elapsedMs, modelRounds = 0, toolCalls = 0, toolExecuted = 0, contentChars = 0, ttftMs = null, pendingTool = null, tokensPerSec = null, subagent = null, localQueue = null }) {
|
|
308
336
|
const s = Math.max(0, Math.round(Number(elapsedMs) / 1000));
|
|
309
337
|
const parts = [
|
|
310
338
|
`已跑 ${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`,
|
|
@@ -316,6 +344,15 @@ export function formatTurnProgress({ phase, elapsedMs, modelRounds = 0, toolCall
|
|
|
316
344
|
if (tokensPerSec != null) parts.push(`≈${Number(tokensPerSec).toFixed(1)} tok/s`);
|
|
317
345
|
parts.push(`已收正文=${contentChars} 字`);
|
|
318
346
|
if (pendingTool) parts.push(`正在等待工具 ${pendingTool.name}(已 ${Math.round(Number(pendingTool.ms) / 1000)}s)`);
|
|
347
|
+
// v0.6.15(§3.59):本地端点因内存压力串行化时,进度行必须能看出来——
|
|
348
|
+
// 否则"在排队"与"引擎卡住"在事后日志里完全同形(真机取证最缺的就是这一行)。
|
|
349
|
+
if (localQueue) {
|
|
350
|
+
parts.push(
|
|
351
|
+
`本地端点已串行化(防内存压力):等待 ${Math.max(0, Number(localQueue.active) || 0)} 个在途请求` +
|
|
352
|
+
(Number(localQueue.queued) > 0 ? `、${Number(localQueue.queued)} 个排队` : '') +
|
|
353
|
+
`,并发上限 ${Number(localQueue.limit) || 1}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
319
356
|
// v0.6.13(问题 3):等 `task` 子代理时必须能看出"这个子任务跑到哪一步"——
|
|
320
357
|
// 否则"等子代理 620s"与"卡死 620s"在日志上完全同形(真机现场正是如此)。
|
|
321
358
|
if (subagent) {
|
|
@@ -753,6 +790,9 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
753
790
|
session: sessionRef?.name ?? null,
|
|
754
791
|
cwd: workingDir,
|
|
755
792
|
permission: cfg.permission ?? permission?.mode ?? null,
|
|
793
|
+
// v0.6.17 §3.64:来源由入口(WebUI/CLI)算好随 cfg 传入;agent 只透传,不猜。
|
|
794
|
+
// 后台任务/REPL 内切换预设等未传来源的路径写 null(来源未知),绝不冒充 config。
|
|
795
|
+
permissionSource: cfg.permissionSource ?? null,
|
|
756
796
|
preset: preset?.name ?? cfg.preset ?? null,
|
|
757
797
|
// v0.6.7(报告二 P3-1):activeCtx 的形状是 { mounted, warnings, promptSections, constraints, toolCount },
|
|
758
798
|
// **没有 `.packs` 键** —— 这里此前恒为 [],账本里"本次运行挂了哪些 Pack"永远记空。
|
|
@@ -859,6 +899,9 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
859
899
|
// 界面/日志据此显示"在等哪个子任务、它跑到第几轮 / 几次工具调用"
|
|
860
900
|
subagent: null,
|
|
861
901
|
forecast: null,
|
|
902
|
+
// v0.6.15(§3.59):本地端点串行化快照(排队时由 provider 的 onQueueWait 填入)
|
|
903
|
+
localQueue: null,
|
|
904
|
+
localQueueNoticed: false,
|
|
862
905
|
noProgressMs,
|
|
863
906
|
lastProgressAt: Date.now(),
|
|
864
907
|
progressReason: '回合开始',
|
|
@@ -1278,6 +1321,12 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1278
1321
|
try {
|
|
1279
1322
|
let promptTokens = 0;
|
|
1280
1323
|
for (const m of sanitized) promptTokens += messageTokens(m, count);
|
|
1324
|
+
// v0.6.15(§3.59 第 3 条):本地端点是否已有在途/排队请求(父回合、子代理、预检共用同一队列)。
|
|
1325
|
+
// 这里读的是**进程级闸门快照**(provider.config.baseUrl),不是猜——云端端点恒为 0。
|
|
1326
|
+
let gate = { active: 0, queued: 0, limit: 1 };
|
|
1327
|
+
try {
|
|
1328
|
+
if (caps.isLocal) gate = localGateSnapshot(provider?.config?.baseUrl);
|
|
1329
|
+
} catch {}
|
|
1281
1330
|
const f = prefillForecast({
|
|
1282
1331
|
promptTokens,
|
|
1283
1332
|
contextWindow: caps.contextWindow,
|
|
@@ -1285,6 +1334,10 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1285
1334
|
firstTokenMs: Number(cfg?.timeout?.firstTokenMs) > 0 ? Number(cfg.timeout.firstTokenMs) : effFirstTokenMs,
|
|
1286
1335
|
budget,
|
|
1287
1336
|
probe: cfg?.endpointProbe || null,
|
|
1337
|
+
localBusy: Boolean(caps.isLocal && (gate.active > 0 || gate.queued > 0)),
|
|
1338
|
+
localActive: gate.active,
|
|
1339
|
+
localQueued: gate.queued,
|
|
1340
|
+
localConcurrency: localConcurrencyLimit(cfg),
|
|
1288
1341
|
});
|
|
1289
1342
|
turnProgress.forecast = f;
|
|
1290
1343
|
// 首次请求一定说;之后只在命中告警线时说(否则每步一条横幅会刷屏)
|
|
@@ -1309,6 +1362,31 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1309
1362
|
// MiniMax P0:仅当 activeModel 声明支持 reasoning 才发送(否则 400 终止回合);'off' 显式禁用例外
|
|
1310
1363
|
reasoningEffort: modelPreset(activeModel)?.supportsReasoning || reasoningEffort === 'off' ? reasoningEffort : undefined,
|
|
1311
1364
|
signal: ac.signal,
|
|
1365
|
+
// v0.6.15(§3.59 第 1 条):本地端点串行化必须**在界面上可见**——
|
|
1366
|
+
// 排队时状态条/进度行写"排队等待本地端点(已串行化防内存压力)",
|
|
1367
|
+
// 拿到名额后恢复"发请求(等首帧)"(onDelta 之后会覆盖成"解码中")。
|
|
1368
|
+
onQueueWait(/** @type {any} */ ev) {
|
|
1369
|
+
if (!ev) return;
|
|
1370
|
+
if (ev.waiting) {
|
|
1371
|
+
turnProgress.localQueue = { active: ev.active, queued: ev.queued, limit: ev.limit };
|
|
1372
|
+
turnProgress.phase = `排队等待本地端点(已串行化防内存压力:${ev.active} 个在途)`;
|
|
1373
|
+
if (!turnProgress.localQueueNoticed) {
|
|
1374
|
+
turnProgress.localQueueNoticed = true;
|
|
1375
|
+
try {
|
|
1376
|
+
io.print(
|
|
1377
|
+
style(
|
|
1378
|
+
`⏳ 本地端点串行化:等待 ${ev.active} 个在途请求(并发上限 ${ev.limit})——` +
|
|
1379
|
+
`本地模型内存压力,父回合与子代理不再同时 prefill`,
|
|
1380
|
+
C.dim
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
} catch {}
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
turnProgress.localQueue = null;
|
|
1387
|
+
if (String(turnProgress.phase || '').startsWith('排队等待本地端点')) turnProgress.phase = '发请求(等首帧)';
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1312
1390
|
onDelta(/** @type {any} */ d) {
|
|
1313
1391
|
io.stopSpinner();
|
|
1314
1392
|
// v0.6.13(A):帧/字符计数 + 首帧时延(TTFT)——本地模型最关键的指标
|
|
@@ -1362,7 +1440,11 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1362
1440
|
// MacBook 本地 507 memory_refusal 根因(v0.4.5):服务端内存拒绝不是「空输出」——
|
|
1363
1441
|
// 直接终结合合并透出降级提示,不计入空轮、不注入续写重试(内存未释放必再 507,空烧请求)。
|
|
1364
1442
|
const e = /** @type {any} */ (err);
|
|
1365
|
-
|
|
1443
|
+
// v0.6.15(§3.59 第 2 条):`engine shed its caches and stays up` / `allocator wall`
|
|
1444
|
+
// 说明引擎**活着**(自己丢缓存后继续服务)——不走这条"507 旧文案"(那里建议重启引擎,
|
|
1445
|
+
// 对活着但内存吃紧的引擎是错误处置);交给下面 describeUpstreamError 的独立分型。
|
|
1446
|
+
const engineShedCaches = /engine shed its caches and stays up|allocator wall/i.test(String(e?.message || ''));
|
|
1447
|
+
if (!engineShedCaches && (e?.status === 507 || /memory_refusal|内存不足|内存拒绝/i.test(String(e?.message || '')))) {
|
|
1366
1448
|
stripOrphanCalls();
|
|
1367
1449
|
markOutcome('done');
|
|
1368
1450
|
return {
|
|
@@ -1515,22 +1597,18 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1515
1597
|
...extra,
|
|
1516
1598
|
});
|
|
1517
1599
|
let args = /** @type {any} */ (null);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1600
|
+
// 参数解析/容错/归一化单源在 tools-flow.js 的 parseToolArgs()(v0.6.17,P1-1 第四刀):
|
|
1601
|
+
// 本函数只返回判据,下面三条副作用(渲染拒绝 / 审计 / 回填分块指引)仍按原顺序执行。
|
|
1602
|
+
const parsedArgs = parseToolArgs(tc.function?.arguments);
|
|
1603
|
+
if (!parsedArgs.ok) {
|
|
1521
1604
|
// 参数 JSON 解析失败:回填错误给模型,不拿空参数去执行工具
|
|
1522
|
-
io.renderToolDenied(name, {},
|
|
1523
|
-
if (auditOn) auditEntry({ denied: true, reason:
|
|
1605
|
+
io.renderToolDenied(name, {}, parsedArgs.deniedReason);
|
|
1606
|
+
if (auditOn) auditEntry({ denied: true, reason: parsedArgs.auditReason });
|
|
1524
1607
|
// 审计(长生成截断循环):给出明确分块指引,避免模型反复用单个超大 write 撞输出上限
|
|
1525
|
-
messages.push({
|
|
1526
|
-
role: 'tool',
|
|
1527
|
-
tool_call_id: tc.id,
|
|
1528
|
-
content:
|
|
1529
|
-
'工具参数 JSON 解析失败(大概率是输出超过模型单次上限被截断)。请把内容拆分成多个较小文件或多次调用逐步写入:单个 write 的参数总长控制在 6000 字符以内;先写核心骨架,再逐文件补充。',
|
|
1530
|
-
});
|
|
1608
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: parsedArgs.message });
|
|
1531
1609
|
return null;
|
|
1532
1610
|
}
|
|
1533
|
-
|
|
1611
|
+
args = parsedArgs.args;
|
|
1534
1612
|
|
|
1535
1613
|
// PreToolUse 钩子
|
|
1536
1614
|
const hook = await hooks.pre(name, args);
|
package/src/cli.js
CHANGED
|
@@ -403,7 +403,16 @@ async function main() {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
// agent 使用的配置 = cfg + 预设 overlay(presetTools/permission/参数按预设生效,cfg 本体保持干净)
|
|
406
|
-
const
|
|
406
|
+
const agentCfgBase = Object.keys(presetOverlay).length ? { ...cfg, ...presetOverlay } : cfg;
|
|
407
|
+
// v0.6.17 §3.64:本进程的权限来源随 cfg 进账本(`run.start.permissionSource`)。
|
|
408
|
+
// 顺序与生效值一致:预设 overlay 里有 permission > config.json 的 permission > 内置缺省。
|
|
409
|
+
// 注意**不写回 cfg**(agentCfg 是副本):repl 的 saveConfig(cfg) 绝不能把来源字段持久化。
|
|
410
|
+
const permissionSource = Object.prototype.hasOwnProperty.call(presetOverlay, 'permission')
|
|
411
|
+
? 'preset'
|
|
412
|
+
: cfg.permission !== undefined && cfg.permission !== null
|
|
413
|
+
? 'config'
|
|
414
|
+
: 'default';
|
|
415
|
+
const agentCfg = { ...agentCfgBase, permissionSource };
|
|
407
416
|
|
|
408
417
|
const pc0 = resolveProviderConfig(cfg, modelName);
|
|
409
418
|
// v0.6.0 C4:内网/本机端点(vLLM、Ollama、OneAPI 等)通常不校验密钥,很多也不发放密钥。
|