@migor/cli 0.9.2 → 0.9.4
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/CHANGELOG.md +216 -1
- package/dist/AGENTS.md +10 -7
- package/dist/dev-child.js +94 -0
- package/dist/dev-logic.js +46 -0
- package/dist/dev-machine.js +489 -0
- package/dist/dev-runner.js +8 -1
- package/dist/dev-watch.js +223 -0
- package/dist/dev.js +377 -626
- package/dist/inspector-page.html +94 -10
- package/dist/inspector-routes.js +530 -0
- package/dist/inspector.js +44 -421
- package/dist/markdown.js +20 -1
- package/dist/native-pick.js +168 -0
- package/dist/panel-logic.js +32 -0
- package/dist/templates/package.json +2 -2
- package/dist/templates/src/app.ts +3 -0
- package/dist/templates/src/dev.config.ts +5 -0
- package/dist/templates/src/registry.ts +5 -0
- package/dist/templates/src/session-store.ts +11 -2
- package/dist/templates/src/tools/read-file/index.ts +50 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,198 @@
|
|
|
5
5
|
(0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
|
|
6
6
|
决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
|
|
7
7
|
|
|
8
|
+
## [0.9.4] - 2026-09-26
|
|
9
|
+
|
|
10
|
+
> 本版主题(窗口 `0.9.3 → 0.9.4`):**成本与限制的账本对齐 + 「能不能发」有了判据**。
|
|
11
|
+
> 四块内容:① 成本口径补齐(缓存读/写单价,含 1h 档 = 2×;图片按**尺寸上界**估);
|
|
12
|
+
> ② 三个「旋钮设成 0 就静默失效」的洞改成**构造期响亮失败**;③ 大载荷不再灌进摘要器
|
|
13
|
+
> (`tool_result` **内嵌**图片块与 `tool_use` 参数两处,实测一张 200 000 字符 base64 的截图
|
|
14
|
+
> 此前渲染出 200 086 字符、估算 50 022 token ⇒ 修后 32 字符 / 3 139 token);
|
|
15
|
+
> ④ 评测即发布闸门(配方 + 可拷走的实现 + 守卫)。另含官网的 agent 可读面(每页 `.md` 变体 +
|
|
16
|
+
> `Accept: text/markdown` 内容协商)。
|
|
17
|
+
> **无破坏性变更**:框架公共 API 与脚手架模板形态逐字未变 —— 三个旋钮的 `0` 原本就
|
|
18
|
+
> 「静默不派发 / 拒掉一切 / 流活不过一帧」,现在改成启动期报错,**没有任何本来能工作的配置
|
|
19
|
+
> 会因此变坏**;既有使用者不需要任何动作。
|
|
20
|
+
|
|
21
|
+
### 修复 · 大载荷不再灌进摘要器
|
|
22
|
+
|
|
23
|
+
- **`tool_result` **内嵌**图片块按尺寸上界估、只给占位**:工具返回截图是生产里最常见的入图路径,
|
|
24
|
+
而此前的「渲染 / 摘要侧只给 `[image …]` 占位、不展开 base64」只覆盖**顶层**图片块。
|
|
25
|
+
实测一张 200 000 字符 base64 的截图:渲染出 200 086 字符(含原始 base64)、估算 **50 022 token**
|
|
26
|
+
—— 修后 **32 字符 / 3 139 token**,与顶层图片同一条口径(内嵌与顶层的估算差 ≤ 3 token)。
|
|
27
|
+
- **`tool_use` 的参数同样有界**:同一类「大载荷灌进摘要器」的洞,两处各配回归用例
|
|
28
|
+
(含「小参数不许被截断」的阳性对照),变异验证 4/4 咬人。
|
|
29
|
+
|
|
30
|
+
### 变更 · 三个旋钮的 `0` 从「静默失效」改成构造期报错
|
|
31
|
+
|
|
32
|
+
- **`Scheduler.every.maxInFlight: 0`**:闸门判据是 `inFlight.size >= maxInFlight`,`0` / 负数时
|
|
33
|
+
**恒真** ⇒ 每次 tick 都跳过,周期任务**永不派发且不报错**(实测 60 ms 内派发 0 次、无任何日志)。
|
|
34
|
+
现在与 `AsyncRunner.concurrency` 同款在构造期抛 `TypeError`;`Infinity`(关闸门)与缺省 `1` 不受影响。
|
|
35
|
+
- **`HttpHandlerOptions.maxBodyBytes: 0`**:实测「每个带 body 的请求都 413」(3 条 POST 全部 413)。
|
|
36
|
+
- **`SseWriterOptions.maxBufferedBytes: 0`**:实测「流活不过一帧」。
|
|
37
|
+
- 三处都判 `invalid` + 构造期抛错,并接上 `limits.ts`(「限制旋钮的 0 是什么」的单一真源)的
|
|
38
|
+
`zeroClauseOf` 文案单源。⚠️ 还挡一类事故:`Number('') === 0` —— 空的环境变量会静默变成「拒绝一切」。
|
|
39
|
+
|
|
40
|
+
### 新增 · 成本口径
|
|
41
|
+
|
|
42
|
+
- **`ModelPricing` 新增 `cacheRead` / `cacheWrite`**(缺省读 0.1×、写 1.25×;**1h 档写 2×** 由
|
|
43
|
+
`priceOverrides` 表达),非法乘数(负数 / NaN)同样构造期抛错。
|
|
44
|
+
- **未定价模型会「响」**:turn 上记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、
|
|
45
|
+
可挂 `onUnpricedModel` 回调 —— 此前价格表外的模型成本恒为 0,`maxCostUsd` 这条护栏
|
|
46
|
+
**静默不触发**(现在失效本身是可观测的)。
|
|
47
|
+
- **图片块的 token 按尺寸上界估**:官方按**尺寸**计费(28×28 像素 = 1 visual token),与文件字节数
|
|
48
|
+
无关;框架取「长边缩到 1568px」的上界 3136 token/块 —— 宁可高估(`maxTotalTokens` 提前拦)
|
|
49
|
+
也不低估(护栏迟触发)。
|
|
50
|
+
|
|
51
|
+
### 新增 · 评测即发布闸门(`docs/eval-gate.md` + `examples/eval-gate/`)
|
|
52
|
+
|
|
53
|
+
- 把「这一版能不能发」收成一条判据:**回归**(基线通过 → 这次失败)与**删用例**(基线里有 → 这次没跑)
|
|
54
|
+
判不通过;「基线里本来就失败」**放行**(已知债不拦发布);退出码 `1`(回归)与 `2`(基线坏了)分两档。
|
|
55
|
+
- 配方文档 + 可直接拷走的实现 + 守卫(9 条纯函数断言 + 文档↔示例导出面双向覆盖 + e2e 真喂三份
|
|
56
|
+
被改坏的基线)。**`src/` 零改动** —— 框架的职责到「产出结论」为止。
|
|
57
|
+
|
|
58
|
+
### 新增 · 官网的 agent 可读面(GEO 档 C/D)
|
|
59
|
+
|
|
60
|
+
- **每页 `.md` 变体**:`packages/website/scripts/build-md-variants.mjs` 在构建末尾从**产物**
|
|
61
|
+
`dist/*.html`(与打分器同源)生成同名 `.md`,正文一个字不丢、剥壳集合与打分器一致。
|
|
62
|
+
- **`Accept: text/markdown` 内容协商**:`packages/website/public/_worker.js`(Cloudflare Pages
|
|
63
|
+
advanced mode)只改写带该头的**无扩展名页面路径**,其余(`/llms.txt`、`/robots.txt`、
|
|
64
|
+
`/sitemap.xml`、`/_astro/*`)原样透传,且**任何异常都退回 `env.ASSETS.fetch(request)`**
|
|
65
|
+
—— advanced mode 下所有请求都过它,兜底必须是最外层的那一个。
|
|
66
|
+
|
|
67
|
+
### 仓库自身(不面向使用者)
|
|
68
|
+
|
|
69
|
+
- **`packages/cli/test/inspector.test.mjs` 新增 ⑫「选完文件夹 → 透传给 run 的就是所选目录」**:
|
|
70
|
+
2026-09-24 真用户反馈「选完之后透传给 agent 的不是所选文件夹」,逐环核实后**链路是通的**
|
|
71
|
+
(`createApp` 把显式 `providers` 拼在 discover 结果**之后** ⇒ 模板的 `WORKDIR = opts.workdir`
|
|
72
|
+
赢过目录里扫出来的无 deps 版本;面板 `readControls` 读 DOM 单源,重画只填空值不覆盖已选),
|
|
73
|
+
但**当时没有任何守卫钉着这条链** —— `scripts/e2e-dev.ts` 第 15-ter 步只覆盖 `POST /run`
|
|
74
|
+
**直带** workdir,选择器那一跳(返回值 → 控件 → 请求体)全靠人读码。新用例钉两端接线:
|
|
75
|
+
① **页面接线**:`/api/fs/pick` 的返回值必须写进工作目录控件,且 `readControls` 从**控件**
|
|
76
|
+
取值(不是镜像 `state`);② **服务端接线**:把返回值原样喂给 `/run` ⇒ 钩子收到的 `workdir`
|
|
77
|
+
就是它(且不是回落 `defaultWorkdir`)。反向验证三条各自变红(摘页面接线 / 摘
|
|
78
|
+
`parseRunRequest` 的 workdir 转发 / 把 `readControls` 换成镜像 `state`)。
|
|
79
|
+
|
|
80
|
+
- **`§7 已知边界`(77 行)立守卫**(`tests/docs/boundary-table.test.ts`):§7 是使用者判断
|
|
81
|
+
「我能不能用这个框架」的唯一依据,此前**零守卫**且被 `usage-guide.test.ts` 的解析器结构性排除
|
|
82
|
+
(那条规则只收「首列恰好是一个反引号标识符」的行,§7 首列是散文 ⇒ 整表零采集)。四条判据:
|
|
83
|
+
标识符不悬空 / 行集合↔登记表双向 / `pin={file,marker}` 可证伪 / 三态(`pin` 38 · `choice` 9 ·
|
|
84
|
+
`gap` 30)+ 下限。`gap` 的语义是「这句话今天为真、明天可能为假而没人会发现」,**不是功能缺失**。
|
|
85
|
+
反向验证 8/8 咬人 —— 变异电池当场抓出第一版 A3 是**假守卫**(子串判定,标题加一个字照样绿)。
|
|
86
|
+
- **`killTree` 的平台分派抽成可注入的纯函数**(`packages/cli/src/dev-child.ts` 的 `killPlanFor`):
|
|
87
|
+
win32 那条 `taskkill /T /F` 此前**在任何平台都跑不到**(直接读 `process.platform`,而 CI 是
|
|
88
|
+
ubuntu、macOS 走 POSIX)。行为逐字不变,新增 6 条用例、变异 4/4 咬人。
|
|
89
|
+
- **守卫注册表补登记**:`packages/cli/test/native-pick.test.mjs` 此前没进 `guards.md §1`
|
|
90
|
+
(守卫在、登记缺),已登记并补两条实测反向验证读数。守卫注册表 §1 逐行审计修掉 3 个「假守卫」。
|
|
91
|
+
- **工具链自我描述 ↔ `ci.yml` 互为真值**(`tests/scripts/verify-all-wiring.test.ts`):步骤数 ==
|
|
92
|
+
CI job 名里写死的数字(那是分支保护依赖的必需检查)、每个 job 都被交代、副本不许各说各话。
|
|
93
|
+
- **CI 新增 `docker-image` job(非必需检查)**:`examples/deploy` 的镜像此前**没有任何门禁构建过**
|
|
94
|
+
(docker 在本仓 CI 出现 0 次、本机无 docker)⇒ 「服务有门禁、镜像只有这一次构建」如实写进
|
|
95
|
+
`examples/deploy/README.md` 与 `AGENTS.md`。
|
|
96
|
+
- **对外文章** `docs/articles/trace-first.md`:把核心卖点讲成一条可核对的故事(每个数字都是仓库
|
|
97
|
+
自己能核对的事实:测试全绿数、三个包 dependencies 全空、8 步验证链、镜像只有一次构建)。
|
|
98
|
+
- **推广文案与仓库事实对齐**:修掉 6 类不符(指向别人包的安装命令、会腐烂的时长、硬编码计数、
|
|
99
|
+
写错 scope 的 npm 链接、连不上的域名、**描述了不存在的功能**)。
|
|
100
|
+
|
|
101
|
+
## [0.9.3] - 2026-09-23
|
|
102
|
+
|
|
103
|
+
> 本版主题(窗口 `0.9.2 → 0.9.3`):**dev 面板两个真用户反馈的落地** —— 原生文件夹选择器
|
|
104
|
+
> (浏览器拿不到所选目录的绝对路径 ⇒ 走 CLI 本机进程拉 OS 原生对话框)、模板 `read_file`
|
|
105
|
+
> 能力的 `list_files`(模型终于「知道自己在哪、里面有什么」);另含 **CLI 内部结构治理**
|
|
106
|
+
> (`dev.ts` 隐式状态机 → 显式状态机、`inspector.ts` 路由表外移)与 **四条仓库守卫**。
|
|
107
|
+
> **无破坏性变更**:框架公共 API 与脚手架模板形态逐字未变 —— 新增的是工具与端点,
|
|
108
|
+
> 既有使用者不需要任何动作(老宿主不认识的响应字段照旧忽略)。
|
|
109
|
+
|
|
110
|
+
### 新增 · 原生文件夹选择器 + 模板的 `list_files`
|
|
111
|
+
|
|
112
|
+
- **模板 read-file 能力新增 `list_files` 工具**(与 `read_file` 同一个类、同一个 provider):
|
|
113
|
+
真用户反馈「面板上换了文件夹,agent 行为好像没变」—— 链路本身是通的(workdir 确实注入),
|
|
114
|
+
缺口在**模型感知**:run 的 prompt 不变、菜单里没有「列目录」的工具,模型不知道自己在哪个
|
|
115
|
+
目录、里面有什么,只能瞎猜文件名。`list_files` 输出的**第一行就是工作目录的绝对路径**
|
|
116
|
+
(模型「知道自己在哪」的通道),目录名带 `/` 后缀,输出有上限(200 条)且截断**明示**;
|
|
117
|
+
越界判定与 `read_file` 共用同一道闸(`safeResolve`)。零框架改动。
|
|
118
|
+
- **dev 面板新增「系统选择…」原生文件夹选择器**:浏览器拿不到所选目录的绝对路径,
|
|
119
|
+
所以走 CLI 本机进程拉 OS 原生对话框(契约 `POST /api/fs/pick`),与既有的 `浏览…`
|
|
120
|
+
文本/列表选择并存。客户端断开(刷新 / 关标签页)会收掉在飞的选择框(不留下
|
|
121
|
+
「之后每次都 409」的后遗症);macOS 的取消判定认错误码 `-128`(不随系统语言本地化),
|
|
122
|
+
不认英文文案。
|
|
123
|
+
|
|
124
|
+
### 修复
|
|
125
|
+
|
|
126
|
+
- **面板刷新 / 关标签页后,「系统选择…」不再永久 409**:原来只靠连接断开(`res.on('close')`)
|
|
127
|
+
收掉在飞的原生选择框,而实测(带插桩的产物副本 + 真浏览器)**刷新页面时那条连接还开着**
|
|
128
|
+
—— 服务端收不到 close ⇒ 选择框留在桌面上没人看、之后每次点都报「已有一个文件夹选择框在等」。
|
|
129
|
+
现在面板在 `pagehide` 时显式通知服务端(新端点 `POST /api/fs/pick/cancel`,`sendBeacon` 发的
|
|
130
|
+
**一次新请求**,与服务端看不看得见断开无关),连接断开那条路**保留**作为兜底(进程直接没了 /
|
|
131
|
+
非浏览器客户端断开仍走它)。两条路互补,各有一条确定性用例守着(`packages/cli/test/inspector.test.mjs`)。
|
|
132
|
+
- **复核残留修复一组**(无破坏性变更):`refreshDev` / `refreshSession` 的失败不再静默
|
|
133
|
+
(进可见通道)、徽标过滤失效 token、折叠键混入 sessionId 防张冠李戴、import 反向全覆盖
|
|
134
|
+
扩到 trace-view 产物、`multiTurn` 笔误进 warning 通道、watcher 错误告警、
|
|
135
|
+
`lastError` 不再被通用文案覆盖、也不再**跨代复用**(spawn 时记基线,只有这一代没写出
|
|
136
|
+
新原因才用通用文案;run 成功即把旧错误从告警条摘下)、双 WORKDIR(registry.ts 与 app.ts)
|
|
137
|
+
混用顺序写进注释(同 token 后注册覆盖先注册,拼错顺序会让面板喂的 workdir 被
|
|
138
|
+
`process.cwd()` 静默顶掉)、模板 `safeResolve` 注明不解 realpath 的已知边界等。
|
|
139
|
+
|
|
140
|
+
### 重构(纯结构,零行为变化)
|
|
141
|
+
|
|
142
|
+
- **`packages/cli/src/dev.ts` 1148 → 776 行**:治理前 `devServer()` 是**735 行一个函数、15 个可变
|
|
143
|
+
`let`、40 处写入点、15 个入口**的**隐式**状态机 —— 本轮四条真缺陷(F1 / F4 / G1 / G2)全是它的
|
|
144
|
+
直接后果。现在拆成四件,其中 `dev-machine.ts` 是显式状态机:
|
|
145
|
+
- `dev-logic.ts`(58 行)六个纯判定**单源**(受理闸 / 延后重启 / 退出原因 / 中止幂等 / 能力比较 /
|
|
146
|
+
选择器串行)。判据不再复制 —— `shouldDeferRestart` 直接委托 `canAcceptRun`,
|
|
147
|
+
「两处各写一份 `running || launching`」就是 F1 的复现。
|
|
148
|
+
- `dev-machine.ts`(703 行)`update(state, ev) → { state, effects[] }` **纯函数**(零副作用、
|
|
149
|
+
零 `node:*`):**26 个事件 / 17 种效果 / 28 条事件矩阵单测**。`child`(runner 在不在)与
|
|
150
|
+
`run`(run 在不在飞)建为**两个正交相位**;`run: 'launching'` 进类型 ⇒ 受理闸写成「非 idle 即拒」,
|
|
151
|
+
F1 那种「漏看一个布尔」**结构上写不出来**;effects 是**判别联合对象**(不是闭包)⇒ F4 那种
|
|
152
|
+
「同一条错误广播两帧」也写不出来;进程句柄不进状态(不可序列化,由接线层持有)。
|
|
153
|
+
- `dev-watch.ts`(224 行)/ `dev-child.ts`(82 行):监视件与子进程无状态原语各归各家;
|
|
154
|
+
spawn / stop / restart 三个**执行器**留在接线层 —— 它们要读写机器状态、判进程身份归属。
|
|
155
|
+
- **`packages/cli/src/inspector.ts` 818 → 413,路由表切到新增 `inspector-routes.ts`(545 行)**:
|
|
156
|
+
14 条路由各抽成命名函数。**纯搬移** —— 路由条件与应答状态码分布(`json` 200×8 / 202×3 / 400×8 /
|
|
157
|
+
403×5 / 404×2 / 503×4、`text` 200×2)与拆前**逐条一致**(机械 diff)。三条边界刻意不动:
|
|
158
|
+
`HttpError` 留在服务侧(跟着路由走会让两个模块互相 import **值** ⇒ 运行期成环)、三道鉴权闸
|
|
159
|
+
(Host / Origin / token)留在服务侧(路由只被已放行的请求调用,写进 `RouteCtx` 头注)、
|
|
160
|
+
`DevHooks` 留在 `inspector.ts`(它的形状被另一条用例当文本断言,路由侧的断言改读新文件)。
|
|
161
|
+
- **清掉 `http.ts` 里一条无用的 `case 'notFound':`**(它与 `default:` 之间没有任何语句 ⇒ 等价于
|
|
162
|
+
没有这个 case):全仓 `biome ci` 输出的最后一条 info 归零 —— 留着它,将来新增的 info 就不显眼了。
|
|
163
|
+
|
|
164
|
+
### 仓库自身(不面向使用者)
|
|
165
|
+
|
|
166
|
+
- **四条守卫**把三句「策略声明」变成可执行,`docs/guards.md` §2「待守」随之**清空**(最后一行就是它):
|
|
167
|
+
- **零运行时依赖**(`tests/architecture/no-runtime-deps.test.ts`):断言三个面 —— 源码层
|
|
168
|
+
(三个包的 `src/**` 只 import 相对路径 / `node:` 内置)、元数据层(三个 `package.json` 无非空
|
|
169
|
+
`dependencies`)、范围层(`packages/` 下每个包必须登记)。扫描器在说明符位置**把字符串读出来**
|
|
170
|
+
(只遮蔽则说明符自己被吞、只读取则注释与模板里的假阳性全进来),配防真空护栏 + 5 处真实假阳性
|
|
171
|
+
站点的回归钉。
|
|
172
|
+
- **CLI 结构**(`packages/cli/test/structure.test.mjs`):CLI 是扁平结构、`layering.test.ts` 不适用。
|
|
173
|
+
守 W1 规模棘轮(单文件 / 总量 / 每个文件都登记)、W2 下发浏览器的产物**运行期零 import**、
|
|
174
|
+
W3 源码侧只许 `import type`。⚠️ 面板白屏有**两类**成因、守的是两处 —— W2/W3 守「多了一条依赖」,
|
|
175
|
+
`inspector.test.mjs` 的⑦守「页面 import 了产物里**不存在**的名字」(2026-09-23 真发生:
|
|
176
|
+
`panel-logic.ts` 少四个函数,而 `tsc` 与 `e2e-dev` 全绿 —— 后者走 HTTP API、不加载页面)。
|
|
177
|
+
- **`docs/**` 引用的提交必须真在主干上**(配 CI `fetch-depth: 0`):2026-09-23 真踩过 —— 计划文档
|
|
178
|
+
写了一个**分支提交**的哈希,那批走 squash 合并 ⇒ 它不在 `main` 的祖先链上,在 `main` 的全新克隆里
|
|
179
|
+
`git show` 直接报未知修订,而这条引用一路合进了 `main`(当时 `docs/**` 的提交引用不受任何守卫)。
|
|
180
|
+
判据两道、缺一不可:只认**真能解析成 commit 对象**的反引号 token(`deadbeef` 这类十六进制词
|
|
181
|
+
硬判会误报),再要求它落在主干的祖先链上。
|
|
182
|
+
- **「0 反射」**(官网首屏 `<b>0</b> 反射` / 特性卡「零反射装饰器」):拆三面 —— 全仓 12 个
|
|
183
|
+
`tsconfig*.json` 无一打开 `experimentalDecorators` / `emitDecoratorMetadata`(**根闸**:不开它
|
|
184
|
+
`tsc` 根本不发射 `design:*`)、四个发布面根不把 `reflect-metadata` 当模块说明符、源码不出现
|
|
185
|
+
`Reflect.*Metadata` 那 9 个 API;射程 138 个文件(`src/` + `packages/cli/src/` +
|
|
186
|
+
`packages/cli/templates/` + `packages/trace-view/src/`)。刻意**不禁** `Reflect.ownKeys` /
|
|
187
|
+
`Reflect.apply`(框架在用)与 `Symbol.metadata`(标准装饰器提案自己发射它)—— §2 原先猜的守卫
|
|
188
|
+
形状会当场误判 3 处正当用法。
|
|
189
|
+
- **`docs/plans/2026-09-23-cli-structure.md` 订正四处失效陈述**:① 引用分支提交哈希 → 改合入提交
|
|
190
|
+
(同一条现在由上面的元守卫守着);② 「本机**只能**报 4/8」+「绕法本次**没有**使用」→
|
|
191
|
+
「**默认**报 4/8」,绕法写成正面指引(含「别全局设」这条边界);③ 「在无 shim 的环境跑一次…
|
|
192
|
+
**预期** 8/8」→ **已实测** 8/8(含第 2 / 5 步真的 `clean-dist` 清空重建);④ 绕法里的「**唯一**」
|
|
193
|
+
—— 实测有两条、射程不同(`CODEBUDDY_SAFE_DELETE_ENABLED=0` 只关 safe-delete 这一路 hook、
|
|
194
|
+
**最窄**;`env -u NODE_OPTIONS` 摘掉整个 composer、更宽)。另:§1 加**快照标记**(那一节是治理
|
|
195
|
+
开工前的事实,故意不改写,但点名路由表与 `STATIC` 白名单现在住在 `inspector-routes.ts` ——
|
|
196
|
+
否则读者会去 `inspector.ts` 找一个已经不在那里的东西)。
|
|
197
|
+
- `docs/guards.md`:四条新守卫登记进 §1,文件数 20 → 26(该行是**活的**登记,不是历史记录);
|
|
198
|
+
`AGENTS.md` / `docs/spec.md` §10 / `docs/usage-guide.md` 同源面同步。
|
|
199
|
+
|
|
8
200
|
## [0.9.2] - 2026-09-22
|
|
9
201
|
|
|
10
202
|
> 本版主题(窗口 `0.9.1 → 0.9.2`):**dev 面板的可读性** —— 模型正文按 Markdown 渲染、
|
|
@@ -289,6 +481,27 @@
|
|
|
289
481
|
(改 `.md` 立刻生效),而框架只对**值形态**追加 `REPORT_HINT` ⇒ 函数形态下子代理不知道自己的最终回复
|
|
290
482
|
就是交回主 agent 的交付物,措辞差异没有任何测试看得见。约定现在写在模板自己那份 `system.md` 里。
|
|
291
483
|
|
|
484
|
+
### 修复(#121 第十一轮复核收口 —— **发布时漏记,2026-09-23 补记**)
|
|
485
|
+
|
|
486
|
+
- **`GET /tasks/:id/stream` 新增 `stream.closed` 帧(流级收尾,API 面)**:任务在**别的进程**
|
|
487
|
+
跑、且还**没到终态**时,旧实现发一帧 `stream.unavailable` 之后没人关流 —— 心跳照打、
|
|
488
|
+
连接永挂。现在紧跟一帧 `stream.closed` 并关闭连接。刻意**不**发 `task.end`:
|
|
489
|
+
那是「任务终态」的语义,拿来收尾等于伪造终态。
|
|
490
|
+
- **MCP stdio 连接器:请求在途中被中止时 Promise 永不 settle**(旧实现只删簿记不 reject,
|
|
491
|
+
响应帧按 id 找不到人)⇒ 直接 `await` 连接器 API 的宿主**永久挂起**。现在中止即 reject
|
|
492
|
+
`AbortError`,与 HTTP 连接器同口径。
|
|
493
|
+
- **`AsyncRunner.streamBufferEvents` / `TaskEventStreams.retainTerminal` 的坏值不再静默**:
|
|
494
|
+
`0` / 负数 / 小数 / `NaN` / `±Infinity` 一律**构造期抛 `TypeError`**。旧实现里 `NaN`
|
|
495
|
+
在两条链上失效方向相反且都静默:前者是「每任务内存闸整条不拦」,后者是「无订阅者的
|
|
496
|
+
终态流全被清空」。`0` 的读法两者刻意不同:`streamBufferEvents` = invalid(配置错误),
|
|
497
|
+
`retainTerminal` = disabled(「不留终态流」是有意义的设定)。
|
|
498
|
+
- **指标**:同毫秒连按两次 `reset()`,窗口起点也**严格前进**(`windowStart` 收为 `private`,
|
|
499
|
+
唯一写者是 `reset()`);`dropped()` / `onDrop` 补了可测面(丢弃计数与回调同口径,
|
|
500
|
+
错误 run 永不丢且不计)。
|
|
501
|
+
- 内部:e2e 端口 TOCTOU 修根因(删 `freePort`,改 `PORT=0` + 解析就绪日志,顺带拆掉
|
|
502
|
+
上一轮的 `startExampleRetrying()` 症状补丁);官网守卫把 `og:url` 纳入 URL 口径、
|
|
503
|
+
llms.txt 链接从前缀匹配改为链接目标集合精确比对(旧判定下删掉首页链接照样绿)。
|
|
504
|
+
|
|
292
505
|
### 文档
|
|
293
506
|
|
|
294
507
|
- `docs/usage-guide.md` §2.1 / §2.2(新):脚手架文件分工 + `agentia dev` 的四组输入 / 监视规则 /
|
|
@@ -1246,7 +1459,9 @@
|
|
|
1246
1459
|
首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
|
|
1247
1460
|
框架本体单包;CLI 独立成包(workspaces)。
|
|
1248
1461
|
|
|
1249
|
-
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.
|
|
1462
|
+
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.4...HEAD
|
|
1463
|
+
[0.9.4]: https://github.com/retrychx/agentia/releases/tag/v0.9.4
|
|
1464
|
+
[0.9.3]: https://github.com/retrychx/agentia/releases/tag/v0.9.3
|
|
1250
1465
|
[0.9.2]: https://github.com/retrychx/agentia/releases/tag/v0.9.2
|
|
1251
1466
|
[0.9.1]: https://github.com/retrychx/agentia/releases/tag/v0.9.1
|
|
1252
1467
|
[0.9.0]: https://github.com/retrychx/agentia/releases/tag/v0.9.0
|
package/dist/AGENTS.md
CHANGED
|
@@ -105,7 +105,7 @@ npx agentia --version # CLI 版本(= -v)
|
|
|
105
105
|
| `src/dev.config.ts` | **数据**(不是逻辑):开发期的声明,如 `multiTurn: ['trip-planner']`;见 §2.2 |
|
|
106
106
|
| `src/session-store.ts` | `FileSessionStore`:把多轮对话落成 `.agentia/session.json`(原子写);见 §6.4「对话历史」 |
|
|
107
107
|
| `src/registry.ts` | 显式注册表(`agentia g` 自动维护) |
|
|
108
|
-
| `src/tools/read-file/` |
|
|
108
|
+
| `src/tools/read-file/` | 示例工具(两个):`list_files` 列**工作目录**的内容(输出首行带工作目录绝对路径),`read_file` 读其中一个文本文件(工作目录由 DI 注入,见 §2.2) |
|
|
109
109
|
|
|
110
110
|
**为什么拆**:`agentia dev` 的调试环要**复用同一个工厂**,才能把「这次调哪个能力 / 工作目录是哪个」
|
|
111
111
|
喂进 `createApp`。所以装配必须在 `app.ts` 里、以**函数**形态存在 —— 别把 `createApp(...)` 搬回
|
|
@@ -131,7 +131,7 @@ npx agentia --version # CLI 版本(= -v)
|
|
|
131
131
|
|---|---|
|
|
132
132
|
| **prompt** | 输入框 + `↑`/`↓` 调回历史(像 shell)。面板上「点一下 = 一次**真** run」 |
|
|
133
133
|
| **能力** | 多选,缺省全选。收窄的是**菜单**(主 agent 仍在环里),不是「只可能调它」—— 别的能力 `tools` 里的显式引用仍能调到被排除的那个 |
|
|
134
|
-
| **工作目录** |
|
|
134
|
+
| **工作目录** | 目录选择器(`浏览…`)+ 「系统选择…」原生对话框。**它是这个环的主控件**:任务型 agent 的 prompt 几乎不变,变的是目录。三个点击语义:点**目录名**进去 / 点 `..` 上一层 / **点文件名** = 把它**所在的**目录当工作目录(prompt 为空时顺手把文件名填进去,你写好的话一个字不动);`.git` 这类**隐藏目录**单列在选择器末尾(可达但靠后)。`浏览…` **总是**按输入框里的路径打开 —— 敲了路径再点它就是打开那个路径(不是「再点一次关掉」);点面板外面收起。`系统选择…` 拉 **OS 原生文件夹对话框**(由 CLI 本机进程弹出,因为浏览器拿不到所选目录的绝对路径),选完直接填进输入框。⚠️ 「换了目录」要让**模型**感知到,靠脚手架 `read-file` 能力里的 `list_files`:它输出首行就是工作目录绝对路径 —— 没有这类工具时模型不知道自己在哪、只能瞎猜文件名(行为看起来「没变」) |
|
|
135
135
|
| **多轮** | 跟能力走:`src/dev.config.ts` 里 `multiTurn: [...]` 声明哪些能力该多轮,混选时取 **OR** 且面板**标出来源**(`多轮·trip-planner`) |
|
|
136
136
|
|
|
137
137
|
`npm run dev -- "你的问题"` 可以直接把第一句 prompt 带上(等价于启动后在输入框里敲)。
|
|
@@ -288,7 +288,7 @@ token 的作用是挡住**本机其它进程**,别把它当网络边界:这
|
|
|
288
288
|
| `onTraceEvent` | **增量记账出口的缺省值**:run **进行中**逐笔回调(`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`),给等不了收尾的消费者(面板 / SSE / 任务流)。与 `sinks` 是**两条缝**、可同时配;单次 run 给了自己的那个是**叠加**(应用级在前)而非覆盖。⚠️ **不保证送达**(宿主自己的流断了就断了),也不替代 `sinks` |
|
|
289
289
|
| `maxTotalTokens` | 缺省成本硬管控:整条 run(**含子 agent / skill 子循环**,上限经 `ToolRunContext` 透传)累计 token 上限(可被单次 run 覆盖) |
|
|
290
290
|
| `maxCostUsd` | 缺省成本硬管控:累计成本(美元)上限(**依赖模型在价格表内**,见 `priceOverrides`;未定价模型会留 `usage.unpriced` 事件,所以「护栏有没有真的生效」看得见) |
|
|
291
|
-
| `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }
|
|
291
|
+
| `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }`)。⚠️ **模型名是精确匹配**:Anthropic 每个型号都发两个 id(不带日期的别名 + 带日期的快照,如 `claude-haiku-4-5` / `claude-haiku-4-5-20251001`),**互不相通** —— 用哪个 id 就得按哪个 id 定价,否则 `maxCostUsd` 不触发(会留 `usage.unpriced`,见下)。条目还可带**缓存乘数** `cacheRead` / `cacheWrite`(缺省 `0.1` / `1.25`,相对 `in`;写 `0` 就是**乘数为零** = 该模型的缓存读 / 写不花钱,**不是**「未设」—— 实现用 `??` 而非 `||`):缓存写缺省只对 **5 分钟** 档正确,用了 `cache_control: { ttl: '1h' }` 就写 `cacheWrite: 2`;缓存读的逐模型例外(Opus 5.5 = `0.05`、Fable 5.1 / Mythos 5.1 = `0.025`)同理。**透传给子 agent/skill 的子循环** —— 不会「主 agent 有成本、子 agent 恒 0」。非法单价与非法乘数都在**第一次 llm 调用之前**就失败:run 以 `status: error` 收口(错误记在 run 根 span 上、`totalUsage` 全 0、client 一次都不被调用)—— ⚠️ **不是抛异常给调用方**(`buildPricing` 的错被 `runAgent` 收成 result,别写成 `try/catch` 等它抛) |
|
|
292
292
|
| `onUnpricedModel` | 遇到价格表外的模型时回调(`{ model, spanId }`,每个循环作用域内每模型一次);抛错被吞,**不改变 run 结局**(定价缺失是宿主配置问题)。用它接告警 |
|
|
293
293
|
| `toolTimeoutMs` | 缺省单工具超时(毫秒);超时该条 tool_result 记 is_error,不杀 run |
|
|
294
294
|
| `maxToolConcurrency` | 缺省同回合并行工具上限;不设 = 不限(全并行) |
|
|
@@ -602,9 +602,9 @@ const handler = createHttpHandler(callable, { runner });
|
|
|
602
602
|
|---|---|
|
|
603
603
|
| `authenticate` | 入口鉴权钩子:**除 `/healthz` 与 `/metrics` 外所有路径**都过它,且在**读 body 之前**(未通过就不收 body)。正常返回即通过;抛 `HttpException` 按其 `status`/`body` 回;抛别的错误回 401,原文只进服务端日志。框架**不实现策略**(不读 env、不碰凭据) |
|
|
604
604
|
| `metrics` | 指标出口:给 `metricsSink()`(或任意 `{ render() }` / 返回字符串的闭包)后,`GET /metrics` 回它的 Prometheus 文本。**不鉴权**(拉取端在集群内网);要保护请放反代后面。不给则该路径 404。⚠️ 这只管**渲染** —— 数字要真的累计,必须把**同一个** sink 注册进 `createApp({ sinks: [metrics] })`(它靠 run 收尾投递,不自己埋点),否则 `/metrics` 恒为 0 **且不报错** |
|
|
605
|
-
| `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB |
|
|
605
|
+
| `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB。构造期校验:必须 > 0 或 `Infinity` —— 0 会让**每个带 body 的请求**都 413(`POST /run` / `POST /tasks` / 审批全废),故直接抛错。⚠️ 还挡 `Number('') === 0`:从**空的环境变量**读出来的配置会静默变成「拒绝一切」 |
|
|
606
606
|
| `maxConcurrentRuns` | 同时在跑的 `POST /run` 上限,超限回 503 + `Retry-After`;缺省 32(传 `Infinity` 恢复无上限)。构造期校验:必须 > 0 或 Infinity —— NaN 会让闸门静默失效、0/负数会全部 503,故直接抛错 |
|
|
607
|
-
| `sseMaxBufferedBytes` | SSE 下游积压上限(字节,`res.writableLength` 超过即收口该 SSE 流并 **abort 对应 run** —— 客户端已经不消费了,继续逐 token 生成只是白烧 token);缺省 8 MiB |
|
|
607
|
+
| `sseMaxBufferedBytes` | SSE 下游积压上限(字节,`res.writableLength` 超过即收口该 SSE 流并 **abort 对应 run** —— 客户端已经不消费了,继续逐 token 生成只是白烧 token);缺省 8 MiB。构造期校验:必须 > 0 或 `Infinity` —— 判据是**写入前**的 `pending > 上限`,0 时第 1 帧照写、**第 2 帧必收口**(流活不过一帧),故直接抛错 |
|
|
608
608
|
| `exposeErrors` | 是否把内部异常原文回给调用方;缺省 `false`(细节只进服务端日志) |
|
|
609
609
|
| `runner` | 注入 `AsyncRunner`(共用 store / 并发上限 / `resumePending`);缺省内部 `new AsyncRunner(app)` |
|
|
610
610
|
|
|
@@ -666,9 +666,9 @@ process.on('SIGTERM', async () => {
|
|
|
666
666
|
| `createBudgetPolicy` | 预算策略:超预算先 `trimToolPairs` 编辑,再 `compactMessages` 压缩(带滞回) |
|
|
667
667
|
| `trimToolPairs` | context editing:丢旧 tool 对(按**对数**,`keepToolPairs`) |
|
|
668
668
|
| `compactMessages` | compaction:旧前缀做摘要(摘要器由你注入,框架不替你造 token) |
|
|
669
|
-
| `estimateMessages` | 估算一组消息的 token
|
|
669
|
+
| `estimateMessages` | 估算一组消息的 token(预算决策用,不是精确记账)。⚠️ **图片块按上界估**:块里没有宽高,而官方图片计费按**尺寸**(28×28 像素 = 1 token,与文件字节数无关)⇒ 框架按「长边缩到 1568px」的**上界 3136 token/块**计(宁可高估让 `maxTotalTokens` 提前拦,也不要低估让它迟触发)。未知块按**未截断**负载估(同上理由) |
|
|
670
670
|
| `defaultEstimateTokens` | 缺省的单文本估算函数(CJK 感知启发式:CJK ≈ 1.5 字/token、其余 ≈ 4 字符/token) |
|
|
671
|
-
| `renderMessages` | 把 messages 渲染成纯文本 —— 喂给你注入的 compaction 摘要器(`summarize
|
|
671
|
+
| `renderMessages` | 把 messages 渲染成纯文本 —— 喂给你注入的 compaction 摘要器(`summarize`)用。⚠️ **产物是有界的**:图片块只给 `[image image/png ~150000B]` 这类占位(**含 `tool_result` 正文里嵌套的图片** —— 工具返回截图是常见的入图路径)、未知块截断到 200 字符、**工具参数**截断到 2000 字符,两种截断都**留计数**(不静默丢)—— **绝不展开 base64**(否则等于把整段 payload 当输入 token 发给摘要模型,真金白银 + 摘要质量一起毁)。⚠️ **纯文本**(`text` 块与 `tool_result` 的字符串正文)**刻意不截断**:那是摘要器要读的内容,截掉等于让 compaction 永久丢掉历史 |
|
|
672
672
|
|
|
673
673
|
**per-run 隔离(`ContextPolicy.forRun`)**:策略可能被配成应用级单例(`createApp({ contextPolicy })`)
|
|
674
674
|
被所有 run 复用。带状态的实现(滞回计数、token 缓存等)应实现可选的 `forRun(): ContextPolicy` ——
|
|
@@ -1419,6 +1419,9 @@ const callable = {
|
|
|
1419
1419
|
| 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
|
|
1420
1420
|
| 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;并行子循环(一回合多个子 agent)各自过闸,超支上限是「**每个在飞分支**各一个回合」而非「总共一个回合」;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
|
|
1421
1421
|
| `maxCostUsd` 依赖价格表 | 模型不在价格表内(且未用 `priceOverrides` 覆盖)时成本恒为 0,这条护栏**不触发** —— 要无条件兜底用 `maxTotalTokens`。**失效会响**:turn 上会记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、可回调 `onUnpricedModel` |
|
|
1422
|
+
| 价格表按模型名**精确匹配** | 别名与带日期快照是两个不同的键:内置表收了 `claude-haiku-4-5`,那么 `claude-haiku-4-5-20251001` 就是**未定价**(成本算不出、`maxCostUsd` 不触发,会留 `usage.unpriced`)。照抄官方文档里的带日期 id 最容易踩这一条 —— 要么改用不带日期的别名,要么 `priceOverrides` 里按**你实际传的那个 id** 补一条 |
|
|
1423
|
+
| 缓存写的缺省乘数只对 **5 分钟** 档正确 | 官方是 5m 写 1.25×、**1h 写 2×**、读 0.1×。而 `Usage` 是四项**聚合**的,分不出这一回合的缓存写走的是哪个 TTL(SDK 的 `Usage.cache_creation` 有拆分,框架没把它带上)⇒ 你在块上用了 `cache_control: { ttl: '1h' }` 就必须在 `priceOverrides` 里写 `cacheWrite: 2`,否则成本**低估 37.5%**、`maxCostUsd` 会迟触发。框架自己的调用点都不设 ttl,所以不改也对 |
|
|
1424
|
+
| 图片块的 token 只能估、且按**上界**估 | 块里没有宽高(`source` 只有 base64 的 `data` 或 `url`),而官方图片计费按**尺寸**(28×28 像素 = 1 visual token),与文件字节数**无关** —— 一张 200KB 纯色 PNG 和一张 200KB 细节图的成本能差两个数量级。框架取「长边缩到 1568px」的**上界 3136 token/块**:宁可高估(`maxTotalTokens` 提前拦)也不低估(护栏迟触发)。渲染 / 摘要侧则只给 `[image …]` 占位,**不展开 base64** |
|
|
1422
1425
|
| 工具超时**不强制取消**工具 | `AgentTool.run` 没有 signal 参数,超时首先是「不等了」;副作用可能已发生。想真停:监听 `ToolRunContext.abandoned`(引擎放弃等待时 abort 它)自行收尾。框架自带的 @SubAgent / @Skill 已这么做 —— 超时即中止子循环(在飞请求被掐、不再后台烧 token),capability span 立刻以 error 收尾 |
|
|
1423
1426
|
| 会话只存对话轮次 | `SessionStore` 存「用户输入 + 最终回复」,run 内部的 tool 往返**不进历史**(要完整过程用 `traceToMessages`);且只有**跑成功**的轮次才回写 |
|
|
1424
1427
|
| 同 session 并发 run 要自行串行化 | `SessionStore` 是 **append-only**:并发写不互相覆盖、不丢数据,但**不保证角色交替** —— 两个并发 run 共用同一 sessionId 时,各自追加的轮次可能交错成「连续两条 user」,下一轮 load 出来撞角色交替校验(400)。同一 session 的并发 run 请调用方自行串行化(每 session 一把锁 / 一条队列) |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev 环的**子进程原语**(C 阶段自 `dev.ts` 纯搬移,行为未改 ——
|
|
3
|
+
* 方案 docs/plans/2026-09-23-cli-structure.md §3 C)。
|
|
4
|
+
*
|
|
5
|
+
* 这里只收**无状态的原语**:解析 tsx 的 CLI 入口(`resolveTsxCli`)与杀整棵进程树
|
|
6
|
+
* (`killTree`)。spawn / stop / restart 三个**执行器**留在 `dev.ts` —— 它们要读写
|
|
7
|
+
* `DevMachineState`(派发 `child-spawned` / `child-exit` 等事件、判进程身份归属),
|
|
8
|
+
* 是接线层的活,不是原语。
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
/**
|
|
14
|
+
* 解析 tsx 的 CLI 入口(绝对路径)。
|
|
15
|
+
*
|
|
16
|
+
* ## 为什么不走 `npx tsx`(实测,不是风格偏好)
|
|
17
|
+
*
|
|
18
|
+
* `npx` 是个**包装器**:它自己再 spawn 一层,而**不把 fd 3 的 IPC 通道转发给孙进程**。
|
|
19
|
+
* 后果不是「启动失败」而是**静默**:runner 里 `process.send` 变成 `undefined`,而
|
|
20
|
+
* `send()` 写的是 `process.send?.()`(可选链)—— 于是所有协议消息被无声丢弃:
|
|
21
|
+
* 面板永远等不到 `ready`,`POST /run` 也永远回不来。更坏的是没有 IPC 通道之后事件循环
|
|
22
|
+
* 无事可做,进程会以 **code=0 干净退出**,看起来像「用户代码自己跑完了」。
|
|
23
|
+
*
|
|
24
|
+
* 同一个 hello 脚本的三方对照:
|
|
25
|
+
*
|
|
26
|
+
* | 启动方式 | `typeof process.send` | IPC |
|
|
27
|
+
* |---|---|---|
|
|
28
|
+
* | `node whoami.mjs` | `function` | ✅ 收到 |
|
|
29
|
+
* | `npx tsx whoami.mjs` | `undefined` | ❌ 无(还多出 npx 的 spinner 噪音) |
|
|
30
|
+
* | `node <tsx>/dist/cli.mjs whoami.mjs` | `function` | ✅ 收到 |
|
|
31
|
+
*
|
|
32
|
+
* ## 为什么不 spawn `node_modules/.bin/tsx`
|
|
33
|
+
*
|
|
34
|
+
* Windows 上它是 `.cmd` shim,会绕回 `npm-bin.ts` 专门要避开的那个 CVE-2024-27980 坑。
|
|
35
|
+
* 直接 `node <tsx/dist/cli.mjs>` 跨平台一致,且是**直接子进程** ⇒ IPC 天然可用。
|
|
36
|
+
*
|
|
37
|
+
* 解析顺序:**用户工程优先**(tsx 是脚手架声明的 devDependency,尊重他们的版本 pin),
|
|
38
|
+
* 再退到 CLI 自己的解析位置(monorepo / 全局安装)。两处都没有 ⇒ `null`,调用方响亮报错。
|
|
39
|
+
*/
|
|
40
|
+
export function resolveTsxCli(root, fromCli) {
|
|
41
|
+
for (const base of [join(root, 'package.json'), fromCli]) {
|
|
42
|
+
try {
|
|
43
|
+
return createRequire(base).resolve('tsx/cli');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* 试下一个解析位置 */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** 子进程优雅退出的等待上限,超时补 SIGKILL(kill 必须连整棵树 —— 见 killTree) */
|
|
52
|
+
export const KILL_GRACE_MS = 3_000;
|
|
53
|
+
/** 平台 → 杀树计划(缺省平台由调用方给进程平台) */
|
|
54
|
+
export function killPlanFor(platform, pid, signal) {
|
|
55
|
+
if (pid === undefined)
|
|
56
|
+
return { kind: 'none' };
|
|
57
|
+
if (platform === 'win32') {
|
|
58
|
+
// 不带 /T 就只杀直接子进程,`npx tsx` 的孙进程会留下孤儿 —— 「整棵树」全落在这个开关上
|
|
59
|
+
return { kind: 'taskkill', args: ['/pid', String(pid), '/T', '/F'] };
|
|
60
|
+
}
|
|
61
|
+
return { kind: 'process-group', pid, signal };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 杀掉**整棵**树(执行器;分派在 `killPlanFor`,行为与抽取前**逐字一致**)。
|
|
65
|
+
*
|
|
66
|
+
* `npx tsx` 自己还会 spawn 子进程,只 kill 直接子进程会留下孤儿(照搬 v0.7.0
|
|
67
|
+
* 「`close()` 保证子进程已终止」的纪律)。POSIX 下靠 `detached: true` 把子进程
|
|
68
|
+
* 变成进程组组长,再对**负 pid** 发信号;win32 没有进程组,用 `taskkill /T`。
|
|
69
|
+
*/
|
|
70
|
+
export function killTree(child, signal) {
|
|
71
|
+
const plan = killPlanFor(process.platform, child.pid, signal);
|
|
72
|
+
if (plan.kind === 'none')
|
|
73
|
+
return;
|
|
74
|
+
if (plan.kind === 'taskkill') {
|
|
75
|
+
try {
|
|
76
|
+
spawn('taskkill', plan.args, { stdio: 'ignore' });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* 已经退出了 */
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
process.kill(-plan.pid, signal); // 负 pid = 整个进程组
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
try {
|
|
88
|
+
child.kill(signal);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* 已经退出了 */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev 环(server 侧)的**纯判定** —— 从 `dev.ts` 那台隐式状态机里抽出的「规则」部分。
|
|
3
|
+
* 为什么不进 `panel-logic.ts`:那是**下发浏览器**的模块(STATIC 白名单、W2/W3 守卫),
|
|
4
|
+
* server 侧纯件进去会随产物下发。这里只有判定、没有状态 ——「谁在什么时候调用它」仍由
|
|
5
|
+
* `dev.ts` 的闭包决定(那是方案 B 的事,见 docs/plans/2026-09-23-cli-structure.md)。
|
|
6
|
+
* 判据单源化的理由:F1 / G1 都是「同一判据两处各写一份、改一处漏一处」的产物。
|
|
7
|
+
*/
|
|
8
|
+
/** 受理新 run 的闸(`submitRun` 的 409)。必须**同时**看 `launching`:受理到 run 发出
|
|
9
|
+
* 之间有一次 `await restart`,只判 `running` 会放两个请求进来(F1)。判据只此一份。 */
|
|
10
|
+
export function canAcceptRun(state) {
|
|
11
|
+
return !state.running && !state.launching;
|
|
12
|
+
}
|
|
13
|
+
/** 文件变更该不该延后到 run 收尾(`onFileChange`:true ⇒ 记 `pendingRestart`)。
|
|
14
|
+
* 与受理闸**同一条**忙闲判据 —— 刻意委托、不复制条件(两处各写就会漂,F1)。 */
|
|
15
|
+
export function shouldDeferRestart(state) {
|
|
16
|
+
return !canAcceptRun(state);
|
|
17
|
+
}
|
|
18
|
+
/** 子进程退出的原因文案(G1 的落点):判据是「跟 spawn 那一刻比,这一代有没有写出
|
|
19
|
+
* 新原因」(`lastError !== errBaseline`),**不是** `lastError ?? generic` —— lastError
|
|
20
|
+
* 全程没有清零点(告警条靠它长存),`??` 会把上一代留下的旧错误当成这一代退出的原因
|
|
21
|
+
* (归因误导)。`generic` 由调用方给:「没等到 ready 就退」与「意外退出」文案不同。 */
|
|
22
|
+
export function exitReason(args) {
|
|
23
|
+
const { lastError, errBaseline, generic } = args;
|
|
24
|
+
return lastError !== null && lastError !== errBaseline ? lastError : generic;
|
|
25
|
+
}
|
|
26
|
+
/** 「中止」的幂等判定(`abortRun`):升级计时器在 ⇒ 中止已发出,再点一次**不该**挂
|
|
27
|
+
* 第二个计时器(两个计时器到点会触发两次 restart,第二次打在刚重启好的 runner 上)。 */
|
|
28
|
+
export function abortDecision(args) {
|
|
29
|
+
return args.hasTimer ? 'idempotent' : 'send';
|
|
30
|
+
}
|
|
31
|
+
/** 能力选择变没变(变了要重启进程 —— D8:进程边界是孤儿 MCP 子进程的唯一回收口)。
|
|
32
|
+
* 形状约定:`prev` 的 `null` = 全量、`next` 的 `undefined` = 全量(`normalizeToolSources`
|
|
33
|
+
* 的手笔),两者不交叉配对。比较用 `join('\u0000')` 而非 `join()`:缺省分隔符是逗号,
|
|
34
|
+
* 来源名里若出现逗号会让 `['a,b']` 与 `['a','b']` 撞成同一串。顺序归一是上游的活。 */
|
|
35
|
+
export function sameToolSources(prev, next) {
|
|
36
|
+
if (prev === null)
|
|
37
|
+
return next === undefined;
|
|
38
|
+
if (next === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
return prev.join('\u0000') === next.join('\u0000');
|
|
41
|
+
}
|
|
42
|
+
/** 原生文件夹选择框的串行化(`pickFolder`):同时只许一个在飞 —— 弹两个系统对话框
|
|
43
|
+
* 叠在一起,用户分不清哪个在回应谁,第二个请求 409。 */
|
|
44
|
+
export function pickGate(args) {
|
|
45
|
+
return args.inFlight ? 'reject' : 'accept';
|
|
46
|
+
}
|