mingdao-harness 0.6.33 → 0.6.35

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.
@@ -1,6 +1,6 @@
1
1
  # MingDao-Harness 架构设计
2
2
 
3
- > 版本口径:v0.6.30。本文档覆盖 `src/` 全部 **91** 个模块(`find src -name '*.js' | wc -l` = 91),
3
+ > 版本口径:v0.6.30(v0.7.0 §1.1 增量见下)。本文档覆盖 `src/` 全部 **94** 个模块(`find src -name '*.js' | wc -l` = 94),
4
4
  > 按层给出每个模块的职责与它守住的那条不变量;判定以文件头注释与实际实现为准,不做推断性描述。
5
5
  >
6
6
  > 阅读顺序建议:§2 模块索引(查「某件事归谁管」)→ §3 数据流(看一次回合怎么走)→ §4 已知边界(看它**保证不了**什么)。
@@ -54,11 +54,12 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
54
54
 
55
55
  ## 2. 模块索引
56
56
 
57
- 以下逐文件列出 `src/` 全部 91 个模块。每条格式为「职责 —— 关键不变量/边界」。
57
+ 以下逐文件列出 `src/` 全部 93 个模块。每条格式为「职责 —— 关键不变量/边界」。
58
58
 
59
- ### 2.1 内核循环与上下文(6)
59
+ ### 2.1 内核循环与上下文(7)
60
60
 
61
61
  - `agent.js` —— Agent 核心循环:消息 → 模型(流式)→ 钩子 → 约束 → 权限 → 工具执行 → 回填 → 循环,直至模型给出纯文本;附带子代理(task 工具)、todo 状态、undo 备份仓、Ctrl+C 中断 —— 每回合结局由 `outcome` 单一来源收敛,返回值与账本 `runEnd` 必须同源。
62
+ - `messages.js` —— 文案 / 预测层(v0.7.0 §1.5 第一步,从 `agent.js` **逐字**切出,零 I/O、无状态):TTFT 提示判定与进程级节流状态、无进展看门狗阈值与中止文案、内存降级的两级压缩(`memoryRecoveryTarget`)、端点学习值预告、prefill 外推与发送前规模预告(`prefillEstimateMs` / `prefillForecast`)、上游错误解读(`describeUpstreamError`)、回合进度行(`formatTurnProgress`)、只读档判定(`startsInReadOnlyPhase`)—— 21 个导出由 `agent.js` 用显式列名再导出(`export { … } from './messages.js'`),导入面与迁移前逐字一致;`messages.js` **不 import agent.js**(依赖方向单向、无环),唯一的模块外依赖是协议层的内存压力判据 `isMemoryPressureError`。
62
63
  - `context.js` —— token 计数与预算裁剪:恒保留首条 system,从尾部向前取到预算,被裁剪时插入说明消息;工具结果上限 20000 字符(`TOOL_RESULT_LIMIT`)—— 计数优先走 `tokenizer.js` 精确计数,只在无词表模型上回退启发式;截断按码点安全切分,不留孤立代理项。
63
64
  - `tokenizer.js` —— 精确 tokenizer:内置 DeepSeek 官方词表(`assets/tokenizer-data.json.gz`)做字节级 BPE 计数,内容级缓存使同一文本重复计数为 O(1);非 DeepSeek 模型回退启发式估算(英文≈4 字符/token、CJK≈0.75 token/字)—— 只用于上下文预算计数,不输出 token id;词表加载失败会重试而非终生锁死。
64
65
  - `routing.js` —— 自动模型路由:长文本 + 规划关键词 → planner,极短指令 → executor,其余用 executor 模型做一次约几十 token 的分类(结果按文本哈希 LRU 缓存);执行类会话粘滞,避免 pro⇄flash 抖动破坏缓存前缀 —— 子代理固定走 executor。
@@ -92,9 +93,10 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
92
93
  - `net-policy.js` —— 出网白名单策略:纯函数,不碰 IO、不改全局状态,便于单测与文档化 —— 命中哪条规则、拦还是放,判定只此一份。
93
94
  - `net-guard.js` —— 出网闸门:在 `globalThis.fetch` 收口做出口判定 + 记账,`guardEgress()` 供 `node:http(s)` 路径(safe-fetch、sync 的 rawRequest)复用 —— **默认不安装**(未配置 `config.net` 时完全不介入);只覆盖内核自己发起的请求,不代表整机不出网。
94
95
 
95
- ### 2.4 会话 / 记忆 / 任务(10)
96
+ ### 2.4 会话 / 记忆 / 任务(11)
96
97
 
97
98
  - `session.js` —— 会话持久化:`<home>/sessions/<时间戳>-<随机>.jsonl` 每轮追加,`--continue` 载入最近一次,`--resume` 打开选择器 —— 会话文件是消息结构的唯一权威,检查点等旁挂状态另存侧车文件。
99
+ - `session-events.js` —— 会话事件日志(v0.7.0 §1.1 切片 A):`<session>.events.ndjson` 追加 12 类事件 `{seq,time,type,data}`(session/turn/step/tool/message/usage/compact/interrupt/checkpoint)—— **seq 是唯一关联键**:跨进程短临界区锁内「读尾行 last+1」、单次 append 后回读校验;读侧跳过 torn 行/未知 type 并返回 `skipped`;`replayToMessages()` 是纯函数(`message.append` 提供消息本体、`compact` 重置基线、`interrupt` 延迟补 tool 占位),由事件流重放的消息数组必须与原消息数组逐字节等价;事件文件是**旁路**,`loadSession`/`rewriteSession`/SSE/UI 都不读它,`cfg.eventLog=false` 或 `MINGDAO_EVENT_LOG=0` 可整体回滚。
98
100
  - `session-index.js` —— 会话检索索引:词表倒排 + 按 mtime/size 的增量同步,分片存 `<home>/sessions-index/<2位sha1前缀>.json`(256 片)—— 未变化直接用缓存词表,>8MB 文件跳过并移除条目;中文按 bigram + 单字、查询串同口径分词后 AND 匹配。
99
101
  - `memory.js` —— 长记忆与自主进化:用户记忆 `~/.mingdao/AGENTS.md`(手动 + 会话结束自动提取用户偏好并去重追加)、会话日志 `journal.jsonl`(默认不注入系统提示,`--journal` / WebUI 勾选才取最近 3 条)—— 自动提取用 executor 模型(约几十 token),`autoMemory: false` 可关。
100
102
  - `tasks.js` —— 多会话后台任务面板:`mingdao run` 拉起独立 worker 子进程,状态落 `<home>/tasks/<id>.json`,命令族 `run` / `tasks` / `tasks watch` / `tasks kill` —— taskId 用不可枚举随机值;kill 前必须过 PID 归属校验(见 `proc.js`)。
@@ -105,7 +107,7 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
105
107
  - `prompts.js` —— 系统提示词构建:基础角色 + 用户记忆 + 技能清单 + 项目 AGENTS.md + Pack 提示段(`packPromptBlock`)—— 记忆类内容必须「关不住围栏」,零宽/双向控制字符需剥离;断行/围栏闭合由 `fencedBlock` 统一处理。
106
108
  - `workspace.js` —— 会话级工作空间:登记/切换/重命名/删除,每个会话记住自己的项目目录,多任务并行不串目录 —— 本模块加锁函数**全部**使用异步版 `withFileLock`,因为它正好在 WebUI 请求路径上(同步版会冻结整个事件循环)。
107
109
 
108
- ### 2.5 合规与账本(3)
110
+ ### 2.5 合规与账本(4)
109
111
 
110
112
  - `ledger.js` —— 执行账本:一次回合 = 一个文件,事件流(`runStart` / `toolCall` / `constraint` / `permission` / `usage` / `netEgress` / `runEnd`)可导出、可校验、可回放 —— 三条不可让步的纪律:写入即脱敏、摘要替代原文(`*Digest`)、降级必须可见;`runEnd` 额外写封条侧车文件,使「尾部截断但链内自洽」可检出。
111
113
  - `audit.js` —— 工具调用审计日志:每次调用(含被拒/被钩子阻止/参数解析失败)落 `~/.mingdao/audit.jsonl`(600),纯追加 + >20000 行保留最近 10000 行 —— 记录做轻量脱敏,`config.audit: false` 可关(默认开)。
@@ -117,9 +119,11 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
117
119
  - `sync.js` —— 云同步客户端:会话 JSONL 跨设备同步,配置在 `config.json` 的 `sync`(无秘密),凭证在 `credentials.json` 的 `sync`(600)—— 冲突规则绝不丢数据:push 前先把远端备份为 `.server-<时间戳>.jsonl`,pull 时本地不动、远端写 `.remote-<时间戳>.jsonl`;`auto` 静默推送失败不影响对话。
118
120
  - `sync-server.js` —— 云同步服务端(零依赖 `node:http/https`):账号 + 设备 token + 会话存储,数据布局 `<DATA_DIR>/users.json`、`devices.json`、`data/<用户名>/sessions/` —— 密码存 `sha256(salt:password)`,设备 token 只存哈希;注册开关 `open` / `invite` / `closed`。
119
121
 
120
- ### 2.7 Web 层(13)
122
+ ### 2.7 Web 层(16)
121
123
 
122
124
  - `web/server.js` —— WebUI 服务器:零依赖 `node:http` + SSE,复用 `createAgent` + `createPermission` + `createWebIO` —— 非回环绑定强制访问令牌(参数/配置/本次随机生成,`timingSafeEqual` 比对);Host 白名单防 DNS rebinding;同一会话并发回合由进程级 `busySessions` 串行化。
125
+ - `web/auth.js`(v0.7.0 §1.5)—— WebUI 访问控制的**纯函数层**(从 `server.js` 逐字切出):`resolveWebToken`(参数 > 配置 > 非回环随机生成)、`extractRequestToken`(`?token=` > `X-MingDao-Token` > `Authorization: Bearer`)、`resolveAuthDecision`(放行/拒绝 + `via` 通道)、`tokenEquals`(`timingSafeEqual`,长度不等直接 false)、`isTrustedHost`(Host 白名单防 DNS rebinding)、`authStartupNotice`(启动文案)—— **不读全局、不写 `res`**;401/403 仍由 `routes/api.js` 唯一写入。
126
+ - `web/provider-cache.js`(v0.7.0 §1.5)—— Provider 缓存 / 预检运行时(从 `server.js` 逐字切出,依赖注入工厂):`createProviderCache(deps)` 提供 `providerFingerprint`(代际号 + baseUrl + apiKey 的 sha256 摘要 + timeout 配置)、`getProviderFor`、`ensureProbe`(单次 15s 上限 / TTL 备忘 / 失败只降级)、`invalidateProviderCache`(实例与探测备忘**一起清**)与 `start`(缺 Key 告警 + 本地端点观察者 + 初始 provider)—— 缓存键含出网指纹,配置一变新任务绝不拿旧实例(H-04)。
123
127
  - `web/routes/api.js` —— API 编排器:访问控制(token / Host / CSRF)→ 静态壳资源 → 按域分发 —— 跨站浏览器请求按 `Sec-Fetch-Site` 一律 403。
124
128
  - `web/routes/domains/config.js` —— 配置域:`/api/state` `/api/config` `/api/models-config`,服务商 Key 管理、自定义模型增删改、API 地址覆盖、系统状态快照 —— `endpointChangeGuard` 防「改地址把 Key 送到别处」。
125
129
  - `web/routes/domains/sessions.js` —— 会话域:`/api/sessions` `/api/session` `/api/draft`,列表/搜索、载入(聚焦工作空间)、重命名/删除、按会话隔离的草稿槽。
@@ -133,7 +137,7 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
133
137
  - `web/web-io.js` —— WebUI 的 io 适配器:把 Agent 的 io 事件翻译为 SSE 事件,权限确认/选择类交互经 `askHandler` 转发到浏览器模态框并等待 `POST /api/permission` 应答。
134
138
  - `web/viewmodel.js`(v0.7.0 §1.2)—— 前端的**纯计算层**:`sideNavLayout`(三档断点下的左栏形态)、`errCardParts`(错误卡文案分解)、`runStripView`(状态条六字段)、`traceRoundView`(轨迹一轮的视图模型)以及它们依赖的格式化助手。**不读 DOM、不读全局**,因此可以被 node 直测;`app.js` 只负责把结果渲染到 DOM。
135
139
  为什么抽出来:此前 `app.js` 3252 行、可执行覆盖 0%,只靠源码正则守卫——UI 改动的"绿"等于"文本没变"。
136
- - `web/util.js` / `web/app.js` / `web/i18n.js` —— 前端:`util.js` 是零状态工具(选择器/转义/Markdown/格式化),`app.js` 是页面主脚本(消息渲染与轨迹、设置面板、自绘气泡与模态框),`i18n.js` 是零依赖 UI 文案表(zh/en 同键、默认跟随 `navigator.language`、`localStorage` 覆盖)—— 地址带 `?token=` 时记入 `sessionStorage` 并从地址栏移除,此后请求统一附加 `X-MingDao-Token`。
140
+ - `web/util.js` / `web/app.js` —— 前端:`util.js` 是零状态工具(选择器/转义/Markdown/格式化),`app.js` 是页面主脚本(消息渲染与轨迹、设置面板、自绘气泡与模态框)—— 地址带 `?token=` 时记入 `sessionStorage` 并从地址栏移除,此后请求统一附加 `X-MingDao-Token`。
137
141
 
138
142
  ### 2.8 CLI 层(13)
139
143
 
@@ -173,8 +177,8 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
173
177
  - `help.js` —— 帮助文本的唯一来源:CLI 与 REPL 各用 `variant` 声明自己多出来的那几行 —— 两个入口的差异必须是显式的,不允许各存一份正文。
174
178
  - `index.js` —— 库公共 API:导出 `@stable`(minor 内向后兼容)与 `@experimental` 两组能力,契约与示例见 DEVELOPER.md。
175
179
 
176
- 模块计数核对:6 + 8 + 12 + 10 + 3 + 2 + 13 + 13 + 19 = **86** 个条目(含合并条目;`find src -name '*.js' | wc -l` = **90**),其中 §2.7 的
177
- `web/util.js` / `web/app.js` 一条覆盖 2 个文件、`web/viewmodel.js` 单列,故条目覆盖 **91** 个 `src/**/*.js`(每版本以 `find` 实测为准),
180
+ 模块计数核对:7 + 8 + 13 + 11 + 4 + 2 + 16 + 13 + 19 = **93** 个条目(含合并条目;`find src -name '*.js' | wc -l` = **94**),其中 §2.7 的
181
+ `web/util.js` / `web/app.js` 一条覆盖 2 个文件、`web/viewmodel.js` 单列,故条目覆盖 **94** 个 `src/**/*.js`(每版本以 `find` 实测为准),
178
182
  与 `find src -name '*.js' | wc -l` 的结果一致。
179
183
 
180
184
  ## 3. 数据流:一次 `runTurn` 的关键路径
package/docs/CONFIG.md CHANGED
@@ -27,7 +27,8 @@
27
27
 
28
28
  可选字段:`temperature`、`maxOutputTokens`、`includeUsage`(流式请求 usage 统计,个别网关不支持
29
29
  `stream_options` 时设 `false`)、`autoTitle`(自动生成会话标题,默认开)、`notify`(任务桌面通知,默认开)、
30
- `autoCompact`(上下文自动压缩,默认开,见下)、`audit`(工具调用审计,默认开,见下)。
30
+ `autoCompact`(上下文自动压缩,默认开,见下)、`audit`(工具调用审计,默认开,见下)、
31
+ `eventLog`(会话事件日志双写,默认开,见下)。
31
32
 
32
33
  ### WebUI 设置项的「默认值」列(只读,不落盘)
33
34
 
@@ -85,6 +86,26 @@ MingDao 先用 executor 模型(路由关闭时为当前模型)把被裁段
85
86
  中文按 bigram+单字、英文按词,多词 AND 匹配;只有内容变化(mtime/size)的会话才重新
86
87
  分词,删除的会话自动清出索引。索引目录可随时删除(下次搜索自动重建)。
87
88
 
89
+ ## 会话事件日志(v0.7.0 §1.1)
90
+
91
+ 每个会话在消息文件旁多一条 append-only 的事件流 `<session>.events.ndjson`(0600,与
92
+ `<session>.jsonl` 同级):每条 `{seq,time,type,data}`,seq 单调、是 turn/step/tool/message
93
+ 之间的唯一关联键。当前 12 类事件:`session.start` / `turn.start` / `turn.end` / `step.start` /
94
+ `step.end` / `tool.start` / `tool.end` / `message.append` / `usage` / `compact` / `interrupt` /
95
+ `checkpoint`。
96
+
97
+ - **双写、旁路**:`appendMessages()` 在会话消息落盘**之后**追加 `message.append`;agent 在
98
+ turn/step/工具/用量/压缩/中断边界追加结构事件。事件日志写失败只 warn,**不影响会话写入**,
99
+ 也不改变 `loadSession()` / `rewriteSession()` 的返回值。
100
+ - **确定性重放**:`replayToMessages(events)` 由事件流派生消息数组(`compact` 重置基线、
101
+ `interrupt` 补 tool 占位),已对拍单轮 / 工具调用 / 压缩 / 中断四个场景与消息数组 deepEqual。
102
+ - **回滚开关**:`"eventLog": false`(默认开)或环境变量 `MINGDAO_EVENT_LOG=0`。关闭后**完全**
103
+ 停止事件写入(含 `message.append` 双写),会话文件照常写入。
104
+ - **兼容**:旧会话没有 events 文件时读取返回空流;事件侧车不会出现在会话列表/最近会话中;
105
+ 事件文件不参与云同步。SSE 帧与 UI 在本切片**未改**。
106
+ - **已知边界**:手动 `/compact` 走 `rewriteSession` 时没有对应 `compact` 事件;自动标题重命名
107
+ 会话文件不会搬迁事件侧车;这两类留待后续切片(事件日志目前是旁路,不参与既有读取)。
108
+
88
109
  ## 会话级工作空间(P3-4)
89
110
 
90
111
  WebUI 中每个会话记住自己的工作目录:新会话记录创建时的全局工作空间;继续该会话时任务
package/docs/DEVELOPER.md CHANGED
@@ -411,3 +411,18 @@ CI 想"只卡新增盲区"就用当前存量数当基线(例如 `--fail-over 1
411
411
  三个套件都是零外部依赖、只用本机临时目录。
412
412
  - 盲区检查只回答"有没有变异",不回答"变异质量"——`--fail-over` 只能挡住新增盲区,
413
413
  挡不住"给一个 600 行模块写 1 条无关变异"这种凑数。质量仍靠 review 与 `expect` 关键词约束。
414
+
415
+ ### 7.4 覆盖率当前**只报告不判失败**(v0.6.35 起)
416
+
417
+ 实测:同一棵树、同一套 34 个套件、连续三次跑 `npm run coverage`,code-lines 得到
418
+ **73.7% / 72.5% / 69.7%**(覆盖行数差 760 行)。原因不是代码变了,而是套件会 spawn 大量子进程,
419
+ **子进程的 V8 覆盖数据是否被聚合不稳定**。
420
+
421
+ 因此 `npm run coverage` 现在只输出趋势与"棘轮回落"提示,**不返回非零**。要恢复闸门:
422
+
423
+ 1. 让收集确定:固定套件顺序、等待所有子进程退出、校验每个套件都产出了覆盖目录;
424
+ 2. 连续 3 次同树测量差 ≤0.2pp(可作为一条套件断言);
425
+ 3. 设 `COVERAGE_GATE=1` 即可恢复"低于基线即失败"(代码分支仍保留)。
426
+
427
+ > 为什么宁可"只报告"也不留一个随机红的闸门:**被随机关掉的闸门等于没有闸门**,
428
+ > 而且会训练大家忽略红灯。宁可先诚实地承认"现在量不准"。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {