tianshu-mcp 0.5.8 → 0.5.9
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.en.md +22 -0
- package/CHANGELOG.md +22 -0
- package/README.en.md +10 -4
- package/README.md +10 -4
- package/dist/config/schema.js +12 -0
- package/dist/loop/fix-loop.js +43 -13
- package/dist/mcp/formatter.js +3 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server.js +4 -1
- package/dist/tasks/task-manager.js +120 -18
- package/dist/tasks/task.js +46 -0
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
- package/skills/tianshu-mcp/SKILL.md +2 -1
- package/skills/tianshu-mcp/usage-examples.md +38 -4
package/CHANGELOG.en.md
CHANGED
|
@@ -8,6 +8,28 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.5.9] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **The GUI terminal state is no longer a false claim on server exit or restart archiving** ([issue #14](https://github.com/lanlan0811/tianshu-mcp/issues/14)). A GUI agent is an external desktop application and the server **owns no process** for it: after an abort the adapter can at best best-effort click the in-app stop control. The old code wrote "server exited, process terminated" for every active task, applying a statement that only holds for spawn children to GUI agents — the orchestrator was dead, the GUI might still be editing the user's project, and nobody was watching.
|
|
16
|
+
- `persistInterrupted()` now branches on the driver: spawn keeps the original wording; GUI writes the honest outcome derived from the adapter's `guiStop` — "confirmed stopped", "stop unconfirmed, the task in the … window may still be running, please open … and confirm there is no residual run", or "no stop result to confirm" (ZCode and TraeWork never click stop and never report `guiStop`).
|
|
17
|
+
- `shutdownInterrupt()` gives GUI tasks a **globally shared** `guiStopWaitMs` budget (new config, default 15000, overridable in `config.json`) so the best-effort stop and bounded wait can finish before the terminal state is written; spawn tasks keep their 2 s budget. An unconfirmed outcome is stated as such.
|
|
18
|
+
- `initialize()` appends "stop unconfirmed + please check manually" when archiving GUI leftovers and sets `guiResidualUnconfirmed`; an unreadable profile is conservatively treated as GUI.
|
|
19
|
+
- `abortTerminal()` (the competing writer in the shutdown race) now persists `guiStop` plus the structured fields on both branches, and derives the window name from `profile.displayName` (the old hard-coded mapping reported zcode/qoder as "Codex", itself a distortion). `runRes.guiStop` is persisted for every agent instead of only qoder, which previously lost the adapter's stop result in the shutdown race.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- New structured `TaskMeta` fields `interruptedCleanStop` (whether the stop is **confirmed**) and `guiResidualUnconfirmed` (pending manual confirmation after restart archiving). The `query_task` / `list_tasks` meta block gains `guiStopUnconfirmed`, giving the orchestrator one structured reason not to re-dispatch.
|
|
24
|
+
- A manual acknowledgement path with **no new tool**: calling `cancel_task` on a terminal GUI task clears `guiResidualUnconfirmed` and appends a `gui_residual_acknowledged` event, **without changing the terminal status or errorType**.
|
|
25
|
+
- New config `config.json` → `shutdown.guiStopWaitMs` (default 15000): the global upper bound for the GUI stop wait on shutdown, decoupled from `gui.cancelWaitMs` (the cancellation path).
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
|
|
29
|
+
- New unit cases `test/unit/gui-stop-disclosure.test.ts` (three-state disclosure, window-name derivation with the "never strip to empty" fallback, and the guarantee that no input produces "the process has been terminated").
|
|
30
|
+
- New integration cases `test/integration/gui-shutdown-interrupt.test.ts` (the `idle=true` / `idle=false` / no-stop-result shutdown paths, restart archiving plus the `cancel_task` acknowledgement, and spawn tasks being unaffected by the GUI branching).
|
|
31
|
+
- `config-hotreload` gained default and override cases for `guiStopWaitMs`; the running-cancellation case in `codex-flow` now asserts the structured fields as well.
|
|
32
|
+
|
|
11
33
|
## [0.5.8] - 2026-09-23
|
|
12
34
|
|
|
13
35
|
### Changed
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.5.9] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- **server 退出 / 重启归档路径的 GUI 终态不再谎报已停止**([issue #14](https://github.com/lanlan0811/tianshu-mcp/issues/14)):GUI agent 是外部桌面应用,server 对其进程**没有所有权**,abort 后适配器至多"尽力点击界面停止",旧实现统一写「server 退出,进程已终止」是把只对 spawn 子进程成立的断言套到了 GUI 上——编排器已死、GUI 可能仍在改用户项目、且无人观察。
|
|
15
|
+
- `persistInterrupted()` 按 `driver` 分流:spawn 类维持原文案;GUI 类按适配器回报的 `guiStop` 如实落「已确认 … 内运行停止」/「未确认停止,窗口中的任务可能仍在继续,请人工打开 … 确认无残留运行」/「无停止结果可确认」(ZCode、TraeWork 不点停止、不回传 `guiStop`)。
|
|
16
|
+
- `shutdownInterrupt()` 为 GUI 任务提供**全局共享**的 `guiStopWaitMs` 预算(新配置,默认 15000,`config.json` 可覆盖),让"尽力停止 + 有界等待"跑完再落终态;spawn 类保持原 2s 预算。到期未确认时如实标注。
|
|
17
|
+
- `initialize()` 归档重启遗留任务时,GUI 类追加「未确认停止 + 请人工检查」并置 `guiResidualUnconfirmed`;profile 不可读时保守按 GUI 处理。
|
|
18
|
+
- `abortTerminal()`(shutdown 竞态中的另一写方)两个分支都落 `guiStop` 与结构化字段;窗口名改由 `profile.displayName` 派生(旧实现把 zcode/qoder 一律写成 "Codex",本身即失真);`runRes.guiStop` 改为对所有 agent 落盘(此前只写 qoder,其余 GUI agent 的停止结果在 shutdown 竞态里丢失)。
|
|
19
|
+
|
|
20
|
+
### 新增
|
|
21
|
+
|
|
22
|
+
- `TaskMeta` 新增结构化字段 `interruptedCleanStop`(是否**已确认**停止)与 `guiResidualUnconfirmed`(重启归档的待人工确认标记);`query_task` / `list_tasks` 的 meta 块新增 `guiStopUnconfirmed`,编排方据此禁止直接重派。
|
|
23
|
+
- 人工确认路径(**不新增工具**):对已终态的 GUI 任务调用 `cancel_task`,可清除 `guiResidualUnconfirmed` 并追加 `gui_residual_acknowledged` 事件,**不改终态与 errorType**。
|
|
24
|
+
- 新配置 `config.json` → `shutdown.guiStopWaitMs`(默认 15000):server 关闭时 GUI 停止等待的全局上限,与 `gui.cancelWaitMs`(取消路径)解耦。
|
|
25
|
+
|
|
26
|
+
### 测试
|
|
27
|
+
|
|
28
|
+
- 新增单元用例 `test/unit/gui-stop-disclosure.test.ts`(三态披露判定、窗口名派生与"剥空退回原名"、任何输入都不得出现「进程已终止」)。
|
|
29
|
+
- 新增集成用例 `test/integration/gui-shutdown-interrupt.test.ts`(shutdown 的 `idle=true`/`idle=false`/无停止结果三条路径、重启归档 + `cancel_task` 确认清除、spawn 类不受 GUI 分流影响)。
|
|
30
|
+
- `config-hotreload` 补 `guiStopWaitMs` 默认值与覆盖用例;`codex-flow` 的运行中取消用例同步为结构化断言。
|
|
31
|
+
|
|
10
32
|
## [0.5.8] - 2026-09-23
|
|
11
33
|
|
|
12
34
|
### 变更
|
package/README.en.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
Visual acceptance (since v0.5.0, with optional AI content validation since v0.5.4): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.
|
|
11
|
+
Visual acceptance (since v0.5.0, with optional AI content validation since v0.5.4): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.9.en.md>) · [All versions](CHANGELOG.en.md).
|
|
12
12
|
|
|
13
13
|
**Tianshu × AI-Agent orchestration MCP server**
|
|
14
14
|
|
|
@@ -189,7 +189,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
189
189
|
| `query_task` | read | Poll status / progress / log tail |
|
|
190
190
|
| `list_tasks` | read | Filtered history of tasks |
|
|
191
191
|
| `get_task_report` | read | Full text of a verification round's report (`report.md`) |
|
|
192
|
-
| `cancel_task` | write + approval | Cancel a running task: CLI agents kill the process tree; GUI agents click the in-app stop control over CDP and bounded-wait (`gui.cancelWaitMs`, default 15s) for the GUI to go idle, stating so explicitly in the final message when the stop is unconfirmed |
|
|
192
|
+
| `cancel_task` | write + approval | Cancel a running task: CLI agents kill the process tree; GUI agents click the in-app stop control over CDP and bounded-wait (`gui.cancelWaitMs`, default 15s) for the GUI to go idle, stating so explicitly in the final message when the stop is unconfirmed. For a terminal GUI task this call doubles as the manual acknowledgement entry point — after verifying the window holds no residual run, it clears the `guiStopUnconfirmed` marker |
|
|
193
193
|
| `verify_task` | read | Run one verification pass on a task/project path (no source changes) |
|
|
194
194
|
| `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent) |
|
|
195
195
|
| `get_profiles` | read | Inspect agent adapters and executable discovery results |
|
|
@@ -228,6 +228,7 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
228
228
|
| [docs/codex-windows-smoke.en.md](docs/codex-windows-smoke.en.md) | Codex Windows hardware record (incl. verify-fail → auto plan → repair-pass loop) |
|
|
229
229
|
| [docs/release-v0.3.4.en.md](<docs/release-v0.3.4.en.md>) | v0.3.4 release notes (ZCode project/model read-back, initialization recovery, session dispatch confirmation, issues #8/#9/#10) |
|
|
230
230
|
| [docs/qoder-cdp.en.md](docs/qoder-cdp.en.md) | Qoder CN GUI driver: installation discovery and instance reuse, full-path workspaces with native import, `modelSource` and global Model Management reasoning tiers, send/answer checkpoints, liveness judging and same-session repair, hardware evidence and uncovered items |
|
|
231
|
+
| [docs/release-v0.5.9.en.md](<docs/release-v0.5.9.en.md>) | v0.5.9 release notes (truthful GUI terminal state on server exit / restart archiving: wording branches on `guiStop`, the bounded `shutdown.guiStopWaitMs` wait, structured pending fields and the manual acknowledgement entry point; issue #14) |
|
|
231
232
|
| [docs/release-v0.5.8.en.md](<docs/release-v0.5.8.en.md>) | v0.5.8 release notes (the four primary documents rewritten against the code, plus the missing TraeWork probe and three probe scripts; no runtime change) |
|
|
232
233
|
| [docs/release-v0.5.7.en.md](<docs/release-v0.5.7.en.md>) | v0.5.7 release notes (orchestration skill docs rewritten against the code: parameter matrix, default precedence, tier correction and the qoder section; no runtime change) |
|
|
233
234
|
| [docs/release-v0.5.6.en.md](<docs/release-v0.5.6.en.md>) | v0.5.6 release notes (Qoder CN GUI adapter, hardware acceptance scope, macOS research boundary) |
|
|
@@ -279,7 +280,7 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
279
280
|
- **Engineering / CI** ✅
|
|
280
281
|
- GitHub Actions: `CI` (`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`, plus a `visual-browser` real-browser matrix ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24, all green with the v0.5.1 tag) and `Release` (tag-triggered) both green
|
|
281
282
|
- Skill self-install verified idempotent on this machine's real `~/.rivet/skills/tianshu-mcp`
|
|
282
|
-
- npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.
|
|
283
|
+
- npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.9`)
|
|
283
284
|
- **Real Tianshu host integration (DoD #6)** ✅ (2026-09-07)
|
|
284
285
|
- Configured the local mode in the real `D:\Tianshu` desktop host `mcp.servers` → sidecar reported `MCP: 2 servers connected, 10 tools` (including this server's 8 tools), spawned the child process and connected over stdio
|
|
285
286
|
- Exposed and fixed a skill-install source-path bug (fileURLToPath, commit 55cf2d0)
|
|
@@ -395,6 +396,11 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
395
396
|
- Corrected the Kimi Code tier domain, the test baseline (826 passed / 12 skipped across 81 files) and the runtime dependency licence table (Apache-2.0 / ISC), and added a callout for **declared-but-unused profile fields**
|
|
396
397
|
- **Fixed a distribution gap**: `scripts/probe-traework.mjs` was not shipped although the docs tell users to run it → added to `files`, plus the missing `probe:traework` / `probe:zcode` / `probe:codex` scripts
|
|
397
398
|
- See the [v0.5.8 release notes](<docs/release-v0.5.8.en.md>)
|
|
399
|
+
- **M28 — Truthful GUI terminal state (server exit / restart archiving) + v0.5.9** (2026-09-23) — 9 new unit cases + 5 new integration cases (issue #14)
|
|
400
|
+
- **No more false claims**: a GUI agent is an external desktop application and the server owns no process for it, so `persistInterrupted()` and `initialize()` now branch honestly on `guiStop` (confirmed / unconfirmed / no stop result) and **never** write "the process has been terminated", which only holds for spawn children
|
|
401
|
+
- **Bounded wait**: the new `shutdown.guiStopWaitMs` (15 s default, one shared budget) lets the best-effort stop finish before the terminal state is written; both branches of `abortTerminal()` now persist `guiStop` plus the structured fields, and the window name is derived from `profile.displayName`
|
|
402
|
+
- **Manual acknowledgement entry point**: new `TaskMeta.interruptedCleanStop` / `guiResidualUnconfirmed` and the meta-block `guiStopUnconfirmed`; calling `cancel_task` on a terminal GUI task clears the pending marker (no new tool, terminal status unchanged)
|
|
403
|
+
- See the [v0.5.9 release notes](<docs/release-v0.5.9.en.md>)
|
|
398
404
|
|
|
399
405
|
## Agent support status
|
|
400
406
|
|
|
@@ -471,7 +477,7 @@ Behavior and limits:
|
|
|
471
477
|
|
|
472
478
|
| Document | Content |
|
|
473
479
|
|---|---|
|
|
474
|
-
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.
|
|
480
|
+
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.9) |
|
|
475
481
|
| [CONTRIBUTING.en.md](CONTRIBUTING.en.md) | Dev setup, conventions, commit/release flow, adding an agent |
|
|
476
482
|
| [SECURITY.en.md](SECURITY.en.md) | Security model (zero credentials / command whitelist / process & desktop-automation boundaries) and private reporting |
|
|
477
483
|
| [CODE_OF_CONDUCT.en.md](CODE_OF_CONDUCT.en.md) | Contributor Code of Conduct |
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
视觉验收(v0.5.0 起,含 v0.5.4 可选 AI 内容校验):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.
|
|
11
|
+
视觉验收(v0.5.0 起,含 v0.5.4 可选 AI 内容校验):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.9.md>) · [全部版本](CHANGELOG.md)。
|
|
12
12
|
|
|
13
13
|
**天枢 × AI-Agent 编排 MCP server**
|
|
14
14
|
|
|
@@ -185,7 +185,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
185
185
|
| `query_task` | read | 轮询状态 / 进度 / 日志尾 |
|
|
186
186
|
| `list_tasks` | read | 历史任务过滤列表 |
|
|
187
187
|
| `get_task_report` | read | 某轮验收报告全文(`report.md`) |
|
|
188
|
-
| `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI
|
|
188
|
+
| `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
|
|
189
189
|
| `verify_task` | read | 对任务/项目路径做一次验收(不改源码) |
|
|
190
190
|
| `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
|
|
191
191
|
| `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
|
|
@@ -225,6 +225,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
225
225
|
| [docs/kimi-cdp.md](docs/kimi-cdp.md) | Kimi Code GUI 驱动:双渲染进程(主窗口 + `Kimi Browser Overlay`)、工作区完整路径绑定与原生对话框导入、模型三级选择与思考档位、执行模式、运行检测与排障 |
|
|
226
226
|
| [docs/codex-windows-smoke.md](docs/codex-windows-smoke.md) | Codex Windows 真机验收记录(含验收失败→自动生成计划→返修通过闭环) |
|
|
227
227
|
| [docs/qoder-cdp.md](docs/qoder-cdp.md) | Qoder CN GUI 驱动:安装发现与实例复用、完整路径工作区与原生导入、`modelSource` 与模型管理全局思考等级、发送/答题检查点、运行判定与原会话返修、真机证据与未覆盖项 |
|
|
228
|
+
| [docs/release-v0.5.9.md](<docs/release-v0.5.9.md>) | v0.5.9 发布说明(server 退出 / 重启归档的 GUI 终态如实化:按 `guiStop` 分流文案、`shutdown.guiStopWaitMs` 有界等待、结构化待确认字段与人工确认入口;issue #14) |
|
|
228
229
|
| [docs/release-v0.5.8.md](<docs/release-v0.5.8.md>) | v0.5.8 发布说明(四份主文档按代码逐项核对重写 + 补发 TraeWork 探针与三个 probe script;无运行时变更) |
|
|
229
230
|
| [docs/release-v0.5.7.md](<docs/release-v0.5.7.md>) | v0.5.7 发布说明(编排技能文档按代码实况重写:参数兼容矩阵、默认值优先级、档位修正与 qoder 章节;无运行时变更) |
|
|
230
231
|
| [docs/release-v0.5.6.md](<docs/release-v0.5.6.md>) | v0.5.6 发布说明(Qoder CN GUI 适配、真机验收范围与 macOS research 边界) |
|
|
@@ -280,7 +281,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
280
281
|
- **工程 / CI** ✅
|
|
281
282
|
- GitHub Actions:`CI`(`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`,另加 `visual-browser` 真实浏览器矩阵 ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24,随 v0.5.1 tag 全绿)与 `Release`(tag 触发)均绿
|
|
282
283
|
- 技能自检安装已在本机真实 `~/.rivet/skills/tianshu-mcp` 验证生效且幂等
|
|
283
|
-
- npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.
|
|
284
|
+
- npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.9`)
|
|
284
285
|
- **天枢宿主真实接入(DoD #6)** ✅(2026-09-07,[host-integration-record.md](docs/host-integration-record.md))
|
|
285
286
|
- 在真实 `D:\Tianshu` 桌面宿主 `mcp.servers` 配置本地模式 → sidecar `MCP: 2 servers connected, 10 tools`(含本 server 8 工具),spawn 子进程并 stdio 连通
|
|
286
287
|
- 实测暴露并修复技能安装源路径 bug(fileURLToPath,提交 55cf2d0)
|
|
@@ -396,6 +397,11 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
396
397
|
- 修正 Kimi Code 档位取值域、测试基线(826 passed / 12 skipped、81 文件)、运行时依赖许可表(Apache-2.0 / ISC),并新增「声明了但无消费方」的 profile 字段提示
|
|
397
398
|
- **修复分发缺口**:`scripts/probe-traework.mjs` 未随包发布(文档却要求用户运行它)→ 纳入 `files`,并补齐 `probe:traework` / `probe:zcode` / `probe:codex` script
|
|
398
399
|
- 详见 [v0.5.8 发布说明](<docs/release-v0.5.8.md>)
|
|
400
|
+
- **M28 — GUI 终态如实化(server 退出 / 重启归档)+ v0.5.9**(2026-09-23)— 新增 9 单元 + 5 集成用例(issue #14)
|
|
401
|
+
- **不再谎报**:GUI agent 是外部桌面应用、server 对其进程无所有权,`persistInterrupted()` 与 `initialize()` 改为按 `guiStop` 如实分流(已确认停止 / 未确认停止 / 无停止结果),**绝不写只对 spawn 子进程成立的「进程已终止」**
|
|
402
|
+
- **有界等待**:新增 `shutdown.guiStopWaitMs`(默认 15s,全局共享预算)让「尽力停止 + 有界等待」跑完再落终态;`abortTerminal()` 两分支补齐 `guiStop` 与结构化字段,窗口名改由 `profile.displayName` 派生
|
|
403
|
+
- **人工确认入口**:新增 `TaskMeta.interruptedCleanStop` / `guiResidualUnconfirmed` 与 meta 块 `guiStopUnconfirmed`;对已终态 GUI 任务调用 `cancel_task` 可清除待确认标记(不新增工具,不改终态)
|
|
404
|
+
- 详见 [v0.5.9 发布说明](<docs/release-v0.5.9.md>)
|
|
399
405
|
|
|
400
406
|
## Agent 适配现状
|
|
401
407
|
|
|
@@ -473,7 +479,7 @@ run_task(projectPath=/path/to/项目, agentId=codex-cli, task="任务书", autoV
|
|
|
473
479
|
| 文档 | 内容 |
|
|
474
480
|
|---|---|
|
|
475
481
|
| [HANDOFF.md](HANDOFF.md) | 项目交接文档:当前状态快照、架构导览、硬性红线、已知限制、接手建议 |
|
|
476
|
-
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.
|
|
482
|
+
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.9) |
|
|
477
483
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | 开发环境、工程规范、提交与发布流程、如何新增 agent |
|
|
478
484
|
| [SECURITY.md](SECURITY.md) | 安全模型(凭证零管理/命令白名单/进程与桌面自动化边界)与私密报告渠道 |
|
|
479
485
|
| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | 贡献者行为准则 |
|
package/dist/config/schema.js
CHANGED
|
@@ -151,6 +151,18 @@ export const ServerConfigSchema = z.object({
|
|
|
151
151
|
autoInstall: z.boolean().default(true),
|
|
152
152
|
})
|
|
153
153
|
.default({}),
|
|
154
|
+
/**
|
|
155
|
+
* server 关闭路径的预算(issue #14)。
|
|
156
|
+
* `guiStopWaitMs`:`shutdownInterrupt()` 对 GUI agent 任务"尽力点击界面停止 + 有界等待空闲"
|
|
157
|
+
* 的**全局**等待上限(全部 GUI 任务共享一份预算,避免多任务串行拖长退出),到期仍无法确认时
|
|
158
|
+
* 终态如实写「未确认停止」。与 `gui.cancelWaitMs`(cancel_task 路径)解耦:cancel 由调用方
|
|
159
|
+
* 主动等待,shutdown 受进程退出时限约束,因此默认值单独可调。
|
|
160
|
+
*/
|
|
161
|
+
shutdown: z
|
|
162
|
+
.object({
|
|
163
|
+
guiStopWaitMs: z.number().int().positive().default(15_000),
|
|
164
|
+
})
|
|
165
|
+
.default({}),
|
|
154
166
|
});
|
|
155
167
|
/* ---------------- agent-profiles.json ---------------- */
|
|
156
168
|
export const ExecutableDiscoverySchema = z.object({
|
package/dist/loop/fix-loop.js
CHANGED
|
@@ -14,7 +14,7 @@ import { writeRepairPlan } from "./repair-plan.js";
|
|
|
14
14
|
import { writeCodexFixPlan } from "../agents/codex/fixplan.js";
|
|
15
15
|
import { buildFixPrompt } from "../agents/codex/input.js";
|
|
16
16
|
import { extractFailureEvidence } from "../agents/codex/verify.js";
|
|
17
|
-
import { isProjectWorkspace } from "../tasks/task.js";
|
|
17
|
+
import { isProjectWorkspace, guiAppNameOf, guiStopDisclosure, } from "../tasks/task.js";
|
|
18
18
|
import { freezeVisualSnapshot, checkVisualSnapshot } from "../visual/snapshot.js";
|
|
19
19
|
import { visualEvidence } from "../visual/report.js";
|
|
20
20
|
import { withVisualLock } from "../visual/lock.js";
|
|
@@ -136,8 +136,11 @@ export class TaskOrchestrator {
|
|
|
136
136
|
meta.actualModel = runRes.actualModel ?? meta.actualModel;
|
|
137
137
|
meta.actualReasoningLevel = runRes.actualReasoningLevel ?? meta.actualReasoningLevel;
|
|
138
138
|
meta.modelSource = runRes.actualModelSource ?? meta.modelSource;
|
|
139
|
-
meta.guiStop = runRes.guiStop;
|
|
140
139
|
}
|
|
140
|
+
// GUI 停止结果对所有 agent 落盘(issue #14):此前只写 qoder,导致其余 GUI agent
|
|
141
|
+
// 在 shutdown 竞态里丢失适配器已回报的停止结果,只能写"无停止结果可确认"。
|
|
142
|
+
if (runRes.guiStop)
|
|
143
|
+
meta.guiStop = runRes.guiStop;
|
|
141
144
|
if (runRes.session) {
|
|
142
145
|
// 会话锚点按 agent 分槽存放:zcodeSession* 与 kimicodeSession* 语义不同
|
|
143
146
|
// (旧快照里的 zcodeSession* 是 ZCode 会话,拿去 Kimi Code 里定位必然失败)。
|
|
@@ -326,8 +329,9 @@ export class TaskOrchestrator {
|
|
|
326
329
|
* - 用户取消(cancelRequestedAt 已置位 / abortSource=user / 排队中)→ cancelled
|
|
327
330
|
* - server 关闭/EOF/未显式取消的中止 → interrupted
|
|
328
331
|
* - 超时 → failed(timeout) + timeout_killed(委托 timeoutTerminal)
|
|
329
|
-
* guiStop(issue #6
|
|
330
|
-
* idle=false
|
|
332
|
+
* guiStop(issue #6 / #14):GUI 停止结果必须如实写进**终态文案与结构化字段**——
|
|
333
|
+
* `idle=false` 与"无停止结果"(ZCode/TraeWork 无停止能力、或未及尝试)都不得声称已停止。
|
|
334
|
+
* 窗口名按 profile 派生,不再按 agentId 硬编码(旧实现把 zcode/qoder 都写成 "Codex" 是失真的)。
|
|
331
335
|
*/
|
|
332
336
|
async abortTerminal(guiStop) {
|
|
333
337
|
if (this.done)
|
|
@@ -337,30 +341,56 @@ export class TaskOrchestrator {
|
|
|
337
341
|
}
|
|
338
342
|
this.done = true;
|
|
339
343
|
const meta = this.meta;
|
|
344
|
+
// 落结构化停止结果:manager 的 persistInterrupted 在 shutdown 竞态里依赖它决定文案,
|
|
345
|
+
// query_task 也可直接读 meta.guiStop 判断是否已确认停止。
|
|
346
|
+
if (guiStop)
|
|
347
|
+
meta.guiStop = guiStop;
|
|
348
|
+
// 只有 driver=gui 的 agent 才谈「GUI 内运行是否已确认停止」:spawn 子进程由 killTree 终止,
|
|
349
|
+
// 对其套用 GUI 文案本身就是新的失真。
|
|
350
|
+
const disclosure = (await this.isGuiDriver())
|
|
351
|
+
? guiStopDisclosure(meta.guiStop, await this.guiAppName())
|
|
352
|
+
: { clean: true, text: "" };
|
|
353
|
+
meta.interruptedCleanStop = disclosure.clean;
|
|
340
354
|
// 用户取消只认结构化意图。排队中用户取消由 TaskManager.cancel 直接落终态,不会进入本分支;
|
|
341
355
|
// orchestrator 尚在采集基线时 status 仍可能是 queued,server shutdown 不得因此误记为用户取消。
|
|
342
356
|
const isCancelled = Boolean(meta.cancelRequestedAt) || meta.abortSource === "user";
|
|
343
357
|
if (isCancelled) {
|
|
344
358
|
meta.errorType = "cancelled";
|
|
345
359
|
meta.abortSource = "user";
|
|
346
|
-
meta.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const guiName = meta.agentId === "kimicode" ? "Kimi Code" : "Codex";
|
|
350
|
-
meta.lastMessage += guiStop.idle
|
|
351
|
-
? ";GUI 内运行已停止。"
|
|
352
|
-
: `;GUI 内运行未确认停止,${guiName} 窗口中的任务可能仍在继续。`;
|
|
353
|
-
}
|
|
360
|
+
meta.guiResidualUnconfirmed = !disclosure.clean;
|
|
361
|
+
meta.lastMessage =
|
|
362
|
+
(meta.cancelReason ? `已取消:${meta.cancelReason}` : "已取消") + disclosure.text;
|
|
354
363
|
await this.deps.store.updateStatus(meta, "cancelled", meta.lastMessage);
|
|
355
364
|
}
|
|
356
365
|
else {
|
|
357
366
|
meta.errorType = "interrupted";
|
|
358
367
|
meta.abortSource = "shutdown";
|
|
359
|
-
meta.
|
|
368
|
+
meta.guiResidualUnconfirmed = !disclosure.clean;
|
|
369
|
+
meta.lastMessage = `任务已中断(server 退出 / 父进程 EOF / 未显式取消的中止)。${disclosure.text}`;
|
|
360
370
|
await this.deps.store.updateStatus(meta, "interrupted", meta.lastMessage);
|
|
361
371
|
}
|
|
362
372
|
return { status: meta.status, meta, reason: meta.lastMessage, summary: meta.lastMessage };
|
|
363
373
|
}
|
|
374
|
+
/** 该任务是否由 GUI driver 驱动;profile 不可读时保守按 GUI 处理(宁可多提示人工检查)。 */
|
|
375
|
+
async isGuiDriver() {
|
|
376
|
+
try {
|
|
377
|
+
const profile = await this.deps.dataHome.getProfile(this.meta.agentId);
|
|
378
|
+
return profile ? profile.driver === "gui" : true;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** 终态文案里的界面窗口称呼:profile 派生,读取失败回退 agentId(issue #14)。 */
|
|
385
|
+
async guiAppName() {
|
|
386
|
+
try {
|
|
387
|
+
const profile = await this.deps.dataHome.getProfile(this.meta.agentId);
|
|
388
|
+
return guiAppNameOf(profile?.displayName, this.meta.agentId);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return guiAppNameOf(undefined, this.meta.agentId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
364
394
|
async finish(status, errorType, reason) {
|
|
365
395
|
if (this.done)
|
|
366
396
|
return { status, meta: this.meta, reason };
|
package/dist/mcp/formatter.js
CHANGED
|
@@ -47,6 +47,9 @@ export function metaFromTask(meta, extra) {
|
|
|
47
47
|
actualReasoningLevel: meta.actualReasoningLevel,
|
|
48
48
|
modelSource: meta.modelSource,
|
|
49
49
|
guiStop: meta.guiStop,
|
|
50
|
+
// 待确认的两个来源取并集:shutdown 路径的 interruptedCleanStop===false,以及重启归档的
|
|
51
|
+
// guiResidualUnconfirmed。未确认时为 true——编排方据此禁止直接重派。
|
|
52
|
+
guiStopUnconfirmed: meta.guiResidualUnconfirmed === true || meta.interruptedCleanStop === false ? true : undefined,
|
|
50
53
|
lastRunSignal: meta.lastRunSignal,
|
|
51
54
|
...extra,
|
|
52
55
|
};
|
package/dist/mcp/tools.js
CHANGED
|
@@ -58,7 +58,7 @@ export const TOOL_DEFS = [
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
name: "cancel_task",
|
|
61
|
-
description: "取消运行中任务:CLI agent 终止进程树;GUI agent(codex 等)尽力点击界面停止按钮并等待 GUI
|
|
61
|
+
description: "取消运行中任务:CLI agent 终止进程树;GUI agent(codex 等)尽力点击界面停止按钮并等待 GUI 空闲(有界超时),未确认停止时结果中明示。排队中任务直接移除。对已处于终态的 GUI 任务,本调用兼任人工确认入口:人工核实窗口中已无残留运行后调用,可清除 meta 的 guiStopUnconfirmed 待确认标记(不改终态)。",
|
|
62
62
|
inputSchema: CancelTaskParamsSchema,
|
|
63
63
|
capability: "write",
|
|
64
64
|
requireApproval: true,
|
package/dist/server.js
CHANGED
|
@@ -23,11 +23,14 @@ export async function buildServer(opts = {}) {
|
|
|
23
23
|
await dataHome.init();
|
|
24
24
|
const cfg = await dataHome.loadConfig();
|
|
25
25
|
const maxRunning = opts.maxRunningOverride ?? cfg.concurrency?.maxRunning ?? 2;
|
|
26
|
+
// shutdown 预算(issue #14):GUI 任务在 server 退出时的停止等待上限,注入 manager 供
|
|
27
|
+
// shutdownInterrupt() 使用;与 profile 的 gui.cancelWaitMs(取消路径)解耦。
|
|
28
|
+
const guiStopWaitMs = cfg.shutdown?.guiStopWaitMs ?? 15_000;
|
|
26
29
|
const store = new TaskStore(home, logger);
|
|
27
30
|
const registry = new AgentAdapterRegistry(() => dataHome.loadProfiles(), logger);
|
|
28
31
|
const engine = new AcceptanceEngine(store, logger);
|
|
29
32
|
const manager = new TaskManager(store, dataHome, registry, engine, logger, makeBuildCtx({ store, dataHome }));
|
|
30
|
-
await manager.initialize(maxRunning);
|
|
33
|
+
await manager.initialize({ maxRunning, guiStopWaitMs });
|
|
31
34
|
// 技能自检安装(失败仅告警不阻断,§17.5);后台执行,不阻塞握手
|
|
32
35
|
if (!opts.skipSkillInstall && (cfg.skills?.autoInstall ?? true)) {
|
|
33
36
|
void skillSelfInstall(logger).then((r) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ACTIVE_STATUSES, isTerminal, isProjectWorkspace, } from "./task.js";
|
|
1
|
+
import { ACTIVE_STATUSES, isTerminal, isProjectWorkspace, guiAppNameOf, guiStopDisclosure, } from "./task.js";
|
|
2
2
|
import { TaskOrchestrator } from "../loop/fix-loop.js";
|
|
3
3
|
import { genTaskId, nowIso } from "../util/id.js";
|
|
4
4
|
import { normPath } from "../util/path.js";
|
|
@@ -17,6 +17,11 @@ const DEFAULT_WORKSPACE_QUEUE_KEY = "__zcode_default_workspace__";
|
|
|
17
17
|
function queueKeyOf(meta) {
|
|
18
18
|
return isProjectWorkspace(meta) ? meta.projectPath : DEFAULT_WORKSPACE_QUEUE_KEY;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* spawn 类任务在 server 关闭时等待 `killTree` 收尾的预算(issue #14 保留原值)。
|
|
22
|
+
* GUI 类任务不复用该值:其停止等待预算见 `shutdown.guiStopWaitMs`(默认 15s)。
|
|
23
|
+
*/
|
|
24
|
+
const SPAWN_INTERRUPT_SETTLE_MS = 2000;
|
|
20
25
|
export class TaskManager {
|
|
21
26
|
store;
|
|
22
27
|
dataHome;
|
|
@@ -30,6 +35,12 @@ export class TaskManager {
|
|
|
30
35
|
runningCount = 0;
|
|
31
36
|
abortControllers = new Map();
|
|
32
37
|
maxRunning = 2;
|
|
38
|
+
/**
|
|
39
|
+
* server 关闭时 GUI agent 任务的停止等待预算(issue #14):
|
|
40
|
+
* 由 `initialize()` 注入(config.json 的 `shutdown.guiStopWaitMs`),与 profile 的
|
|
41
|
+
* `gui.cancelWaitMs` 解耦——shutdown 受进程退出时限约束,取消路径由调用方主动等待。
|
|
42
|
+
*/
|
|
43
|
+
guiStopWaitMs = 15_000;
|
|
33
44
|
constructor(store, dataHome, registry, engine, logger, buildCtx) {
|
|
34
45
|
this.store = store;
|
|
35
46
|
this.dataHome = dataHome;
|
|
@@ -38,20 +49,67 @@ export class TaskManager {
|
|
|
38
49
|
this.logger = logger;
|
|
39
50
|
this.buildCtx = buildCtx;
|
|
40
51
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 启动前注入并发上限与 shutdown 预算,并归档遗留活动任务(不续跑)。
|
|
54
|
+
* 遗留的 GUI 任务必须如实标注「GUI 内运行未确认停止,请人工检查」(issue #14):
|
|
55
|
+
* 此刻服务器对该 GUI 无任何连接,必然无法确认,绝不写「进程已终止」。
|
|
56
|
+
*/
|
|
57
|
+
async initialize(opts) {
|
|
58
|
+
const { maxRunning, guiStopWaitMs } = typeof opts === "number" ? { maxRunning: opts, guiStopWaitMs: undefined } : opts;
|
|
43
59
|
this.maxRunning = maxRunning;
|
|
60
|
+
if (guiStopWaitMs !== undefined)
|
|
61
|
+
this.guiStopWaitMs = guiStopWaitMs;
|
|
44
62
|
const legacy = await this.store.scanLegacyActive();
|
|
45
63
|
for (const meta of legacy) {
|
|
46
64
|
this.tasks.set(meta.taskId, meta);
|
|
47
65
|
meta.errorType = "interrupted";
|
|
48
66
|
meta.abortSource = "shutdown";
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
// 遗留快照里的 guiStop 是上一次 abort 的结果,不构成本次重启后的确认,先清掉。
|
|
68
|
+
delete meta.guiStop;
|
|
69
|
+
let message = "server 重启遗留(启动时归档,不续跑)。";
|
|
70
|
+
if (await this.isGuiDriver(meta.agentId)) {
|
|
71
|
+
const app = await this.guiAppName(meta.agentId);
|
|
72
|
+
const disclosure = guiStopDisclosure(undefined, app);
|
|
73
|
+
meta.interruptedCleanStop = false;
|
|
74
|
+
meta.guiResidualUnconfirmed = true;
|
|
75
|
+
// 启动归档没有"点击停止"这一步(适配器要求实例归属证明,缺失即 fail-closed),
|
|
76
|
+
// 因此文案用「无停止结果可确认」而不是「未确认停止」。
|
|
77
|
+
message = `server 重启遗留(启动时归档,不续跑)${disclosure.text}`;
|
|
78
|
+
}
|
|
79
|
+
meta.lastMessage = message;
|
|
80
|
+
// 逐个 await:与 shutdownInterrupt 同款的「先写状态再等快照」竞态必须避免,
|
|
81
|
+
// 否则紧随其后的 query_task 可能读到半截 meta。
|
|
82
|
+
await this.store
|
|
83
|
+
.updateStatus(meta, "interrupted", "server 重启遗留归档")
|
|
84
|
+
.catch((e) => this.logger.warn(`遗留任务 ${meta.taskId} 归档失败:${String(e)}`));
|
|
51
85
|
}
|
|
52
86
|
if (legacy.length)
|
|
53
87
|
this.logger.warn(`启动归档 ${legacy.length} 个遗留任务(interrupted)`);
|
|
54
88
|
}
|
|
89
|
+
/** agent 是否由 GUI 驱动(driver="gui")。profile 不可读时保守按 GUI 处理,绝不谎报已停止。 */
|
|
90
|
+
async isGuiDriver(agentId) {
|
|
91
|
+
try {
|
|
92
|
+
const profiles = await this.dataHome.loadProfiles();
|
|
93
|
+
const profile = profiles[agentId];
|
|
94
|
+
if (!profile)
|
|
95
|
+
return true;
|
|
96
|
+
return profile.driver === "gui";
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
this.logger.warn(`读取 agent profile 失败(${agentId}),按 GUI 保守处理:${String(e)}`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** 终态文案里的界面窗口称呼(profile 派生,无法读取时回退 agentId)。 */
|
|
104
|
+
async guiAppName(agentId) {
|
|
105
|
+
try {
|
|
106
|
+
const profiles = await this.dataHome.loadProfiles();
|
|
107
|
+
return guiAppNameOf(profiles[agentId]?.displayName, agentId);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return guiAppNameOf(undefined, agentId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
55
113
|
get activeCount() {
|
|
56
114
|
return this.runningCount;
|
|
57
115
|
}
|
|
@@ -244,6 +302,8 @@ export class TaskManager {
|
|
|
244
302
|
* 返回 settled 表示任务是否已在本调用内落终态(issue #6):活动中任务会 abort 编排
|
|
245
303
|
* 协程,GUI agent 侧 run 协程先尽力点击界面停止按钮并等待空闲(gui.cancelWaitMs,
|
|
246
304
|
* 默认 15s),本方法有界等待其落终态后再返回,取消结果不再"请求即成功"。
|
|
305
|
+
* 对**已是终态**的 GUI 任务,本方法兼任「人工确认消除残留待确认状态」的入口(issue #14),
|
|
306
|
+
* 此时返回 cleared=true(见方法末尾注释)。
|
|
247
307
|
*/
|
|
248
308
|
async cancel(taskId, reason) {
|
|
249
309
|
const meta = await this.getMeta(taskId);
|
|
@@ -305,39 +365,72 @@ export class TaskManager {
|
|
|
305
365
|
reason: "已请求取消,但任务尚未在本调用内落终态(GUI 侧停止可能未完成);请稍后 query_task 复核",
|
|
306
366
|
};
|
|
307
367
|
}
|
|
368
|
+
// 终态任务:不取消,但承担 issue #14 的「人工确认消除」职责——GUI 任务被 shutdown/重启
|
|
369
|
+
// 归档为 interrupted 时会留下「GUI 内运行未确认停止」的待确认状态,人与 AI 检查过 GUI
|
|
370
|
+
// 无残留运行后,用同一个 cancel_task 清除标记(不新增工具)。仅清标记,不改终态与 errorType。
|
|
371
|
+
if (meta.guiResidualUnconfirmed || meta.interruptedCleanStop === false) {
|
|
372
|
+
const app = await this.guiAppName(meta.agentId);
|
|
373
|
+
const note = reason
|
|
374
|
+
? `已确认人工核查(${app})无残留运行:${reason}`
|
|
375
|
+
: `已确认人工核查(${app})无残留运行`;
|
|
376
|
+
meta.guiResidualUnconfirmed = false;
|
|
377
|
+
meta.interruptedCleanStop = true;
|
|
378
|
+
meta.lastMessage = `${meta.lastMessage ?? ""}(${note})`;
|
|
379
|
+
await this.store.appendEvent(meta.taskId, "gui_residual_acknowledged", meta.status, note);
|
|
380
|
+
await this.store.writeSnapshot(meta);
|
|
381
|
+
this.tasks.set(meta.taskId, meta);
|
|
382
|
+
return {
|
|
383
|
+
found: true,
|
|
384
|
+
settled: true,
|
|
385
|
+
cleared: true,
|
|
386
|
+
reason: `任务已处于终态(${meta.status}):${note}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
308
389
|
return { found: true, reason: `任务已处于终态(${meta.status}),无需取消`, settled: true };
|
|
309
390
|
}
|
|
310
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* server 退出:终止全部活动任务并标 interrupted(排队中任务也归档)。
|
|
393
|
+
* GUI agent 是外部桌面应用,server 对其进程没有所有权:abort 后适配器至多"尽力点击界面停止"。
|
|
394
|
+
* 因此这里给 GUI 类任务一份**全局共享**的 `guiStopWaitMs` 预算(issue #14),让适配器的停止逻辑
|
|
395
|
+
* 跑完并把结果经 orchestrator / meta 如实落盘;到期仍无法确认时由 `persistInterrupted` 写「未确认停止」。
|
|
396
|
+
* 绝不写「进程已终止」这类只对 spawn 子进程成立的断言。
|
|
397
|
+
*/
|
|
311
398
|
async shutdownInterrupt() {
|
|
312
399
|
for (const ac of this.abortControllers.values())
|
|
313
400
|
ac.abort();
|
|
314
|
-
//
|
|
401
|
+
// 全局 deadline:N 个 GUI 任务并行等待,退出的最坏耗时仍是 guiStopWaitMs(不是 N 倍)
|
|
402
|
+
const guiDeadline = Date.now() + this.guiStopWaitMs;
|
|
315
403
|
const killPromises = [];
|
|
316
404
|
for (const id of this.running) {
|
|
317
405
|
const meta = this.tasks.get(id);
|
|
318
406
|
if (meta && ACTIVE_STATUSES.includes(meta.status)) {
|
|
319
|
-
killPromises.push(this.persistInterrupted(meta));
|
|
407
|
+
killPromises.push(this.persistInterrupted(meta, guiDeadline));
|
|
320
408
|
}
|
|
321
409
|
}
|
|
322
410
|
await Promise.all(killPromises);
|
|
323
411
|
// 排队中任务归档
|
|
324
|
-
for (const
|
|
412
|
+
for (const meta of this.tasks.values()) {
|
|
325
413
|
if (meta.status === "queued") {
|
|
326
414
|
meta.status = "interrupted";
|
|
327
415
|
meta.errorType = "interrupted";
|
|
328
416
|
meta.abortSource = "shutdown";
|
|
329
417
|
meta.lastMessage = "server 退出,排队中任务已归档";
|
|
330
418
|
await this.store.updateStatus(meta, "interrupted", meta.lastMessage).catch(() => { });
|
|
331
|
-
void taskId;
|
|
332
419
|
}
|
|
333
420
|
}
|
|
334
421
|
this.running.clear();
|
|
335
422
|
this.runningCount = 0;
|
|
336
423
|
this.queue.clear();
|
|
337
424
|
}
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
|
|
425
|
+
/**
|
|
426
|
+
* 有界等待子进程/停止动作收尾后落 interrupted(server 关闭路径,不得误记为 cancelled)。
|
|
427
|
+
* - spawn 类:沿用 2s 预算(给 `killTree` 收尾);
|
|
428
|
+
* - GUI 类:等到共享的 `guiDeadline`,把适配器的停止结果如实写进文案与字段(issue #14)。
|
|
429
|
+
* 若 orchestrator 已在本预算内落了终态,则直接返回,保留它更精确的文案。
|
|
430
|
+
*/
|
|
431
|
+
async persistInterrupted(meta, guiDeadline) {
|
|
432
|
+
const gui = await this.isGuiDriver(meta.agentId);
|
|
433
|
+
const deadline = gui ? guiDeadline : Date.now() + SPAWN_INTERRUPT_SETTLE_MS;
|
|
341
434
|
for (;;) {
|
|
342
435
|
if (!ACTIVE_STATUSES.includes(meta.status))
|
|
343
436
|
return; // orchestrator 已落终态
|
|
@@ -345,13 +438,22 @@ export class TaskManager {
|
|
|
345
438
|
break;
|
|
346
439
|
await new Promise((r) => setTimeout(r, 50));
|
|
347
440
|
}
|
|
348
|
-
if (ACTIVE_STATUSES.includes(meta.status))
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
441
|
+
if (!ACTIVE_STATUSES.includes(meta.status))
|
|
442
|
+
return;
|
|
443
|
+
meta.status = "interrupted";
|
|
444
|
+
meta.errorType = "interrupted";
|
|
445
|
+
meta.abortSource = "shutdown";
|
|
446
|
+
if (gui) {
|
|
447
|
+
const app = await this.guiAppName(meta.agentId);
|
|
448
|
+
const disclosure = guiStopDisclosure(meta.guiStop, app);
|
|
449
|
+
meta.interruptedCleanStop = disclosure.clean;
|
|
450
|
+
meta.guiResidualUnconfirmed = !disclosure.clean;
|
|
451
|
+
meta.lastMessage = `server 退出${disclosure.text}`;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
352
454
|
meta.lastMessage = "server 退出,进程已终止";
|
|
353
|
-
await this.store.updateStatus(meta, "interrupted", meta.lastMessage).catch(() => { });
|
|
354
455
|
}
|
|
456
|
+
await this.store.updateStatus(meta, "interrupted", meta.lastMessage).catch(() => { });
|
|
355
457
|
}
|
|
356
458
|
enqueue(meta) {
|
|
357
459
|
const key = queueKeyOf(meta);
|
package/dist/tasks/task.js
CHANGED
|
@@ -58,3 +58,49 @@ export function isProjectWorkspace(meta) {
|
|
|
58
58
|
export function isDefaultWorkspace(meta) {
|
|
59
59
|
return workspaceModeOf(meta) === "default";
|
|
60
60
|
}
|
|
61
|
+
/* ---------------- GUI 停止结果的如实披露(issue #14) ---------------- */
|
|
62
|
+
/**
|
|
63
|
+
* 由 profile.displayName 派生「界面窗口称呼」,供终态文案使用。
|
|
64
|
+
* 剥掉括号内的形式说明与「桌面端 / 客户端 / Desktop」等通用后缀——写进文案的是窗口名,
|
|
65
|
+
* 不是 profile 文档里的自我介绍;无 displayName 时回退 agentId。
|
|
66
|
+
* 纯字符串处理,无平台/文件系统依赖。
|
|
67
|
+
* @param displayName profile.displayName(可能缺失)
|
|
68
|
+
* @param agentId 回退用的 agentId
|
|
69
|
+
*/
|
|
70
|
+
export function guiAppNameOf(displayName, agentId) {
|
|
71
|
+
const raw = (displayName ?? "").trim() || agentId;
|
|
72
|
+
// 只剥"说明性质"的括号段(含 GUI/桌面/客户端/Desktop 等描述词),以及这些通用后缀词本身。
|
|
73
|
+
// 刻意不做无限剥离:像「TraeWork(测试)」这类有实际意义的括号段必须保留;
|
|
74
|
+
// 凡是剥空的都不采纳,退回原名——文案可以长,不能失真。
|
|
75
|
+
const stripped = raw
|
|
76
|
+
.replace(/[((]([^(())]*)[))]/g, (whole, inner) => /gui|desktop|桌面|客户端|应用|窗口/i.test(inner) ? "" : whole)
|
|
77
|
+
.replace(/\b(desktop|gui)\b/gi, "")
|
|
78
|
+
.replace(/桌面端|桌面版/g, "")
|
|
79
|
+
.replace(/\s{2,}/g, " ")
|
|
80
|
+
.replace(/^[\s\-–—·]+|[\s\-–—·]+$/g, "")
|
|
81
|
+
.trim();
|
|
82
|
+
return stripped || raw.replace(/\s{2,}/g, " ").trim();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* GUI 停止结果的如实披露(红线 8 的单一实现)。
|
|
86
|
+
* 判定矩阵:`idle === true` → 已确认停止;`idle === false` → 点击过但未确认;
|
|
87
|
+
* `undefined` → 无停止结果(该 agent 无停止能力或未及尝试)——后两者都**不得**声称已停止。
|
|
88
|
+
* @param stop 适配器回报的 guiStop;缺失即无停止结果
|
|
89
|
+
* @param app 界面窗口称呼(见 guiAppNameOf)
|
|
90
|
+
* @returns undefined 表示无需披露(非 GUI agent 调用方自行判断)
|
|
91
|
+
*/
|
|
92
|
+
export function guiStopDisclosure(stop, app) {
|
|
93
|
+
if (stop?.idle === true) {
|
|
94
|
+
return { clean: true, text: `;已确认 ${app} 内运行停止。` };
|
|
95
|
+
}
|
|
96
|
+
if (!stop) {
|
|
97
|
+
return {
|
|
98
|
+
clean: false,
|
|
99
|
+
text: `;${app} 内运行无停止结果可确认,窗口中的任务可能仍在继续,请人工打开 ${app} 确认无残留运行。`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
clean: false,
|
|
104
|
+
text: `;${app} 内运行未确认停止,窗口中的任务可能仍在继续,请人工打开 ${app} 确认无残留运行。`,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -179,7 +179,7 @@ meta 的 `needsUserKind` 给出等待类型,`pendingQuestion` 给出问题原
|
|
|
179
179
|
| `succeeded` | 验收通过(或未开验收且 agent 正常退出) | `get_task_report(taskId, round?)` 取 changedFiles / diffstat / checks,向用户汇报 |
|
|
180
180
|
| `failed` | 未开自动返修时的失败,或**硬失败**(`errorType=spawn`) | 读 `agentEndReason`(§9)区分:硬失败按表中处置;真失败给**针对性** feedback 调 `rework_task` |
|
|
181
181
|
| `needs_attention` | ①自动返修轮次用尽仍失败;②验收**阻塞**(配置/完整性错误、缺基准、页面不可达等);③GUI 空闲/超时/断线(`errorType=agent_failed`/`timeout`) | 先读报告定位,再决定 `rework_task` 或转人工 |
|
|
182
|
-
| `cancelled` / `interrupted` | 取消 / 超时或中断 | 看 `abortSource`(`user`/`shutdown`/`timeout`/`internal
|
|
182
|
+
| `cancelled` / `interrupted` | 取消 / 超时或中断 | 看 `abortSource`(`user`/`shutdown`/`timeout`/`internal`);GUI 任务另看 `guiStopUnconfirmed`(见下) |
|
|
183
183
|
|
|
184
184
|
关键区别:
|
|
185
185
|
|
|
@@ -189,6 +189,7 @@ meta 的 `needsUserKind` 给出等待类型,`pendingQuestion` 给出问题原
|
|
|
189
189
|
- **`running` 卡死**才用 `cancel_task`:CLI agent 终止进程树;GUI agent 尽力点击界面停止并等待空闲(有界超时),取消文案会如实标注 GUI 侧是否已停。
|
|
190
190
|
**标注“未确认停止”时不要重派同项目任务**——窗口内可能仍在跑,重派护栏也会以 `instance_busy` 拒绝。
|
|
191
191
|
- **`needs_user` 状态下取消**:run 协程已退出、CDP 已断开,MCP 无法再点 GUI 停止按钮,文案会提示人工检查。
|
|
192
|
+
- **`interrupted` + `guiStopUnconfirmed=true`(server 退出 / 宿主 EOF / 重启归档)**:`tianshu-mcp` 对 GUI 进程没有所有权,**“编排器已停”不等于“窗口里的任务已停”**。先让用户人工打开对应窗口确认没有还在跑的 turn,再调 `cancel_task(taskId, reason="已人工核对窗口无残留运行")` 清除待确认标记(终态仍是 `interrupted`),**之后**才可安全重派同项目任务。详见 `usage-examples.md` §9.5.1。
|
|
192
193
|
|
|
193
194
|
汇报纪律:**不要反复空转重试**。多次仍不过或不可修时,如实汇报 `errorType`/`agentEndReason`、失败 check 与输出尾部、变更清单,并给建议(人工看报告 / 换 agent / 缩小任务)。
|
|
194
195
|
|
|
@@ -278,7 +278,8 @@ run_task(projectPath=/path/to/项目, agentId=codex-cli,
|
|
|
278
278
|
| `zcodeSessionId` / `qoderSessionId` / `boundProjectPath` | 会话锚点与项目绑定回执(kimicode 的锚点仅在服务端保留,不回显) |
|
|
279
279
|
| `modelProvider` / `permissionMode` | 实际生效的供应商标识与权限模式(zcode 等) |
|
|
280
280
|
| `actualModel` / `actualReasoningLevel` / `modelSource` | 实际生效模型、等级与模型来源(qoder) |
|
|
281
|
-
| `guiStop` |
|
|
281
|
+
| `guiStop` | 最近一次中断时 GUI 停止的点击与空闲确认结果(`clicked` / `idle`);`idle=true` 才是**已确认**停止 |
|
|
282
|
+
| `guiStopUnconfirmed` | 出现即为 `true`:GUI 任务的终态**未确认**停止(`guiStop.idle=false` 或重启归档无任何确认手段),重派前必须先人工确认并用 `cancel_task` 消除(§9.5.1) |
|
|
282
283
|
| `progressSummary` / `lastRunSignal` | 轮询期进度摘要 / 最近运行信号 |
|
|
283
284
|
| `finishedAt` | 终态落定时间 |
|
|
284
285
|
| `model` / `mode` | 本次派单传入的模型 / 面板模式 |
|
|
@@ -502,13 +503,38 @@ continue_task(taskId=tsk_..., message={"选择开发语言":"TypeScript","需要
|
|
|
502
503
|
```text
|
|
503
504
|
cancel_task(taskId, reason="用户要求停止")
|
|
504
505
|
→ 返回 meta.message 可能为:
|
|
505
|
-
"
|
|
506
|
-
"
|
|
506
|
+
"已取消:…;已确认 <窗口名> 内运行停止。" ← 已确认停止,可安全重派
|
|
507
|
+
"已取消:…;<窗口名> 内运行未确认停止,窗口中的任务可能仍在继续,请人工打开 <窗口名> 确认无残留运行。" ← 需人工检查
|
|
508
|
+
"已取消:…;<窗口名> 内运行无停止结果可确认,…(ZCode / TraeWork 无停止能力)" ← 需人工检查
|
|
507
509
|
"已取消(等待用户处理时):…;GUI 内可能仍有等待中的会话,请人工检查。"
|
|
508
510
|
"已取消 …(尚未落终态:GUI 侧停止可能未完成,请稍后 query_task 复核)"
|
|
509
511
|
```
|
|
510
512
|
|
|
511
|
-
GUI agent 取消语义:尽力点击界面停止按钮并等待 GUI 空闲(有界超时)。**未确认停止前不要重派同项目任务**——重派护栏会以 `instance_busy` 拒绝派发(防止新旧 turn 交叠),宁可等人工确认。也可看 meta 的 `guiStop`(`clicked` / `idle
|
|
513
|
+
GUI agent 取消语义:尽力点击界面停止按钮并等待 GUI 空闲(有界超时)。**未确认停止前不要重派同项目任务**——重派护栏会以 `instance_busy` 拒绝派发(防止新旧 turn 交叠),宁可等人工确认。也可看 meta 的 `guiStop`(`clicked` / `idle`)与 `guiStopUnconfirmed`:`guiStopUnconfirmed=true` 或 `guiStop.idle=false` 即未确认停止。
|
|
514
|
+
|
|
515
|
+
### 9.5.1 server 退出 / 重启后的 GUI 残留确认(issue #14)
|
|
516
|
+
|
|
517
|
+
```text
|
|
518
|
+
# server 退出(宿主退出 / stdio EOF)后重新接入:
|
|
519
|
+
query_task(taskId)
|
|
520
|
+
→ meta.status = "interrupted",meta.abortSource = "shutdown"
|
|
521
|
+
文案二选一:
|
|
522
|
+
"server 退出;已确认 <窗口名> 内运行停止。" ← 停止已确认,可安全重派
|
|
523
|
+
"server 退出;<窗口名> 内运行未确认停止,窗口中的任务可能仍在继续,请人工打开 <窗口名> 确认无残留运行。"
|
|
524
|
+
"server 退出;<窗口名> 内运行无停止结果可确认,…" ← 无停止能力(zcode / traework)
|
|
525
|
+
meta.interruptedCleanStop = false、meta.guiStopUnconfirmed = true
|
|
526
|
+
|
|
527
|
+
# 重启归档(上一进程被 kill,来不及落终态):
|
|
528
|
+
→ 文案为 "server 重启遗留(启动时归档,不续跑);… 请人工打开 <窗口名> 确认无残留运行。"
|
|
529
|
+
meta.guiResidualUnconfirmed = true
|
|
530
|
+
|
|
531
|
+
# 人工打开该窗口,确认没有还在跑的 turn 之后:
|
|
532
|
+
cancel_task(taskId, reason="已人工核对窗口无残留运行")
|
|
533
|
+
→ 清除待确认标记(追加 gui_residual_acknowledged 事件),status 仍是 interrupted
|
|
534
|
+
→ meta.guiStopUnconfirmed 变为不存在、meta.guiResidualUnconfirmed = false
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**顺序纪律**:`guiStopUnconfirmed` 为真时**先人工确认、再重派**同项目任务——`tianshu-mcp` 对 GUI 进程没有所有权,`interrupted` 只说明**编排器**不再观察,不等于窗口里的任务已停。服务端在 shutdown 时会给 GUI 任务一份全局共享的 `shutdown.guiStopWaitMs`(默认 15 秒)预算去"尽力停止 + 有界等待",但到期仍无法确认时只会如实标注,**不会**替你断言已停止;重启归档更是**不会**自动去点停止(无会话锚点、对无归属证明的实例 fail-closed)。
|
|
512
538
|
|
|
513
539
|
### 9.6 agent-profiles.json 相关配置(可选)
|
|
514
540
|
|
|
@@ -532,6 +558,14 @@ GUI agent 取消语义:尽力点击界面停止按钮并等待 GUI 空闲(
|
|
|
532
558
|
- `defaultAutoFixRounds`:该 agent 的自动返修缺省轮数(codex 5 / zcode 2 / kimicode 2 / qoder 3)。
|
|
533
559
|
- 整键覆盖语义:数据目录 `agent-profiles.json` 里同名键会**覆盖**内置 profile 的对应字段;用户自定义 profile(如 `codex-cli`)会出现在 `get_profiles` 中。
|
|
534
560
|
|
|
561
|
+
### 9.7 server 级配置(config.json,可选)
|
|
562
|
+
|
|
563
|
+
```json
|
|
564
|
+
{ "shutdown": { "guiStopWaitMs": 15000 } }
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
- `shutdown.guiStopWaitMs`:server 退出时,GUI agent 任务「尽力点击界面停止 + 有界等待空闲」的**全局共享**上限(默认 15000 = 15 秒,全部 GUI 任务共用一份预算,退出耗时不随任务数增长)。spawn 类任务不受影响(固定 2 秒等 `killTree` 收尾)。到期仍未确认空闲时,终态如实写「未确认停止」并置 `guiStopUnconfirmed`;调大它可以让退出时更容易等到确认结果,代价是退出(以及宿主关闭)变慢。
|
|
568
|
+
|
|
535
569
|
---
|
|
536
570
|
|
|
537
571
|
## 10. 汇报模板
|