dsh-openwrite 0.2.18 → 0.2.20

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.
Files changed (45) hide show
  1. package/README.md +22 -5
  2. package/docs/ISSUE_76_REPAIRS.md +61 -0
  3. package/docs/RELEASE_ACCEPTANCE.md +10 -1
  4. package/package.json +1 -1
  5. package/packages/openwrite-bridge/lib/client.d.ts +1 -0
  6. package/packages/openwrite-bridge/lib/client.js +26 -4
  7. package/packages/openwrite-bridge/lib/contracts-generated.d.ts +1 -1
  8. package/packages/openwrite-bridge/lib/contracts-generated.js +1 -2
  9. package/packages/openwrite-bridge/lib/domain.js +2 -0
  10. package/packages/openwrite-bridge/lib/index.js +6 -6
  11. package/packages/openwrite-bridge/lib/managed-runtime.js +37 -37
  12. package/packages/openwrite-bridge/lib/tools.js +198 -32
  13. package/packages/studio-panel/lib/client.js +488 -488
  14. package/presets/openwrite/agent.cordis.yml +10 -2
  15. package/presets/openwrite/skills/canon/SKILL.md +2 -0
  16. package/presets/openwrite/skills/dialoguequality/SKILL.md +2 -0
  17. package/presets/openwrite/skills/dog-delivery-query/SKILL.md +2 -0
  18. package/presets/openwrite/skills/dog-import-query/SKILL.md +2 -0
  19. package/presets/openwrite/skills/dog-review-query/SKILL.md +2 -0
  20. package/presets/openwrite/skills/export-book/SKILL.md +2 -0
  21. package/presets/openwrite/skills/foreshadow/SKILL.md +2 -0
  22. package/presets/openwrite/skills/foreshadowing-system/SKILL.md +2 -0
  23. package/presets/openwrite/skills/learn/SKILL.md +2 -0
  24. package/presets/openwrite/skills/novel-creator/SKILL.md +2 -0
  25. package/presets/openwrite/skills/novel-reviewer/SKILL.md +2 -0
  26. package/presets/openwrite/skills/oh-story-deslop/SKILL.md +2 -0
  27. package/presets/openwrite/skills/oh-story-long-analyze/SKILL.md +2 -0
  28. package/presets/openwrite/skills/oh-story-long-scan/SKILL.md +2 -0
  29. package/presets/openwrite/skills/oh-story-long-write/SKILL.md +2 -0
  30. package/presets/openwrite/skills/oh-story-review/SKILL.md +2 -0
  31. package/presets/openwrite/skills/progress/SKILL.md +2 -0
  32. package/presets/openwrite/skills/review-chapter/SKILL.md +2 -0
  33. package/presets/openwrite/skills/revise-span/SKILL.md +2 -0
  34. package/presets/openwrite/skills/style-system/SKILL.md +2 -0
  35. package/presets/openwrite/skills/truth-validation/SKILL.md +2 -0
  36. package/presets/openwrite/skills/workflow-manager/SKILL.md +2 -0
  37. package/presets/openwrite/skills/world-query/SKILL.md +2 -0
  38. package/presets/openwrite/skills/write-next/SKILL.md +2 -0
  39. package/release/{openwrite-5.8.9-py3-none-any.whl → openwrite-5.8.10-py3-none-any.whl} +0 -0
  40. package/release/runtime-manifest.json +9 -6
  41. package/release/source.json +3 -3
  42. package/vendor/dsh-dog/lib/core.js +1 -1
  43. package/vendor/dsh-dog/lib/index.d.ts +1 -1
  44. package/vendor/dsh-dog/lib/index.js +202 -4
  45. package/vendor/dsh-dog/lib/{core-DdfMcrB4.js → core-BAlgZpLq.js} +2 -2
package/README.md CHANGED
@@ -29,14 +29,12 @@
29
29
 
30
30
  ## 快速安装
31
31
 
32
- **0.2.13 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2**。0.2.0-rc.2 已完成 Web 工作台渲染与 DoG 数据读取验证,Windows Electron 桌面端启动/恢复链路尚未实机复测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。默认使用 npm `latest` 渠道;商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
32
+ **0.2.20 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2**。三个版本均已完成 Web 工作台渲染与 DoG 数据读取验证,Windows Electron 桌面端启动/恢复链路尚未实机复测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。默认使用 npm `latest` 渠道;商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
33
33
 
34
34
  npm 包和对应 GitHub Release 使用同一份验收产物。也可安装精确版本的 Release 包:
35
35
 
36
- **0.2.13 的 GitHub 安装包已发布,npm `latest` 暂时仍为 0.2.12,等待本次发布的 2FA 验证后同步发布。** 0.2.12 不包含 #57 的创作页刷新循环修复;请使用下方已发布的 0.2.13 Release 包。
37
-
38
36
  ```sh
39
- dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.13/dsh-openwrite-0.2.13.tgz
37
+ dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.20/dsh-openwrite-0.2.20.tgz
40
38
  dsh web
41
39
  ```
42
40
 
@@ -66,6 +64,10 @@ dsh web
66
64
 
67
65
  ### dsh 0.2 桌面端升级
68
66
 
67
+ `0.2.20` 捆绑 Core 5.8.10,修复 [#76](https://github.com/LiPu-jpg/Openwrite/issues/76) 的草稿恢复、任务状态、编辑和审稿契约,并补充 [#65](https://github.com/LiPu-jpg/Openwrite/issues/65) 的英文使用支持。行为与范围见 [修复说明](docs/ISSUE_76_REPAIRS.md)。正式可下载版本以 npm `latest` 和 GitHub Release 为准。
68
+
69
+ `0.2.19` 候选包修复两个加载/工具问题:部分安装下「OpenWrite 创作」预设从列表消失、DoG 工具不可用(dsh-dog 宿主包解析失败,[#74](https://github.com/LiPu-jpg/Openwrite/issues/74)),以及 `novel_*` 工具的对象/数组参数在模型输出 JSON 字符串时预览失败、`data` 字段丢失([#75](https://github.com/LiPu-jpg/Openwrite/issues/75))。正式可下载版本以 npm `latest` 和 GitHub Release 为准。升级后需停止并重新启动 dsh,再刷新浏览器页面。
70
+
69
71
  `0.2.18` 候选包修正 `auto:cheapest`(Core 5.8.9):零价模型不再参与比价,自动落到付费低价模型上。正式可下载版本以 npm `latest` 和 GitHub Release 为准。升级后需停止并重新启动 dsh,再刷新浏览器页面。
70
72
 
71
73
  `0.2.17` 候选包新增自动选模(Core 5.8.8):模型档案的模型名写成 `auto:cheapest` 会自动选用服务商价格目录中总价最低且满足上下文/输出要求的模型(目录缓存一小时,价格变动自动跟随),写成 `auto:popular`(OpenRouter)则由官方 `openrouter/auto` 按社区用量自动选模型。正式可下载版本以 npm `latest` 和 GitHub Release 为准。升级后需停止并重新启动 dsh,再刷新浏览器页面。
@@ -101,7 +103,7 @@ dsh web
101
103
 
102
104
  已有会话不会自动切换预设。详细操作与工作区规则见 [使用流程](docs/WORKFLOWS.md)。
103
105
 
104
- **界面语言**:面板已内置中文与英文词典,跟随 dsh 语言设置。切换方法:Settings → General → Language → English(选择后存为 `locale.preference`)。注意 Agent 的 persona、技能说明与部分后端状态/报错信息仍以中文为主;Agent 会用你在对话中使用的语言回复。
106
+ **界面语言**:面板已内置中文与英文词典,跟随 dsh 语言设置。切换方法:Settings → General → Language → English(选择后存为 `locale.preference`)。Agent 的 persona 和所有技能入口要求用作者的对话语言回复;运行环境与任务状态提供英文,Studio 报错附带英文说明和稳定错误码。已有作品内容不会自动翻译,正文按作者指定的语言生成。
105
107
 
106
108
  ## 配置模型
107
109
 
@@ -165,3 +167,18 @@ conductor/ 连续写章、评审与修订编排
165
167
  Logo 基于 OpenWrite `native-core` 分支,已调整深浅主题配色;基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),图谱集成使用 [dsh-dog](https://github.com/Fun10165/dsh-dog)。
166
168
 
167
169
  生态目录:[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) · [社区插件市场](https://dshtui.com/plugins/)。OpenWrite 当前面向 DeepSeek Harness 的 `web` profile,使用 `dsh web` 打开创作工作台;尚未验证 dsh-TUI 界面兼容性。
170
+
171
+ ## Language / 语言
172
+
173
+ In dsh, choose **Settings → General → Language → English** (`locale.preference`).
174
+ The OpenWrite panel follows the host language. The writing agent and slash-command
175
+ help follow your conversation language; manuscripts, outlines and assets retain
176
+ their existing language. You can say “Please write and review this novel in English.”
177
+ The bundled Chinese skill files are procedural instructions, not an output-language
178
+ restriction. Runtime setup messages use English. Backend failures include stable
179
+ `code`, English `message`, `request_id`, and the original diagnostic text.
180
+
181
+ 在 dsh 的 **设置 → 通用 → 语言 → English** 切换面板语言。Agent 的回复和命令帮助跟随
182
+ 当前对话语言;正文与资产遵循作品已有语言,除非作者要求翻译。
183
+
184
+ 任务恢复、编辑确认及审稿职责范围见 [可靠性修复说明](docs/ISSUE_76_REPAIRS.md)。
@@ -0,0 +1,61 @@
1
+ # Reliability and language changes (issues #76 and #65)
2
+
3
+ This change targets the deterministic failures reported against plugin 0.2.19 / Core 5.8.9. It does not calibrate literary scores or make model reviews deterministic. Automated regression tests use isolated registries and fake model responses.
4
+
5
+ ## Writing and recovery
6
+
7
+ `novel_write_chapter` now submits `chapter_write` to the durable task queue and returns `task_id` immediately. Poll `novel_task_get`. A transport error cannot prove rejection: use `novel_tasks_list(since=<ISO timestamp>)` or `novel_chapter_run_action(action="list", since=...)` before submitting again. Transport diagnostics store endpoint, timestamp and call identity, without credentials or prompt content.
8
+
9
+ If commit fails, generated prose remains recoverable. New manuscripts remain on disk with a `.uncommitted.json` sidecar; an existing manuscript is restored and the generated replacement is saved separately. The error includes `run_id_v2`, `draft_artifact`, and `draft_path`. Resume with `novel_write_chapter(chapter_id=..., run_id_v2=...)` or `resume_from_draft=true`. Recovery reuses the completed draft and facts, verifies the input revisions, and retains its original target and model provenance. Reconcile stale acceptance first when requested. `novel_task_retry` also resumes eligible drafts and completed continuous tasks with failed chapters.
10
+
11
+ `PROJECT_BUSY` defers the task and retries after two seconds. It does not consume the continuous failure budget. Cancellation is cooperative: `cancel_requested=true` means requested; `cancelled=true` means the persisted task is actually cancelled. `wait_seconds` waits up to 30 seconds; otherwise poll. In-flight model calls may finish before cancellation is observed, but cancelled runs reject late commits.
12
+
13
+ The chapter target contract is **200–50000**, with precedence **explicit argument → chapter outline → 3000**. Preview and write return `target_words_source` and the effective range, **−20% / +50%**. Resume retains the draft's target; changing it requires a fresh generation. Outline chapter metadata is validated on structural edits, document writes and change plans; volume totals have separate limits. Runs retain `model_profile`, `requested_model`, actual `model` and `provider`; differing model resolution is flagged.
14
+
15
+ ## Acceptance and editing
16
+
17
+ A `STALE_ACCEPTED_REVISION` error includes the operation handle and recovery actions. Inspect the operation before choosing:
18
+
19
+ - `resume` continues an incomplete acceptance using its frozen input.
20
+ - `abandon`, with `operation_id` and `confirm=true`, releases only that operation's pending heads and retains its audit record. It does not delete or silently accept manuscript text. A real unaccepted manuscript still requires baseline/external acceptance.
21
+ - `external`, with `confirm=true`, explicitly accepts the currently observed manuscript as a new source revision. This supersedes the prior pending revision; it is a deliberate re-baseline, not a reread or a retry.
22
+
23
+ Capture rejects missing chapters. `acknowledge` supports outline, foreshadowing, canon, chapter_memory, character_state, runtime_facts, review, plan, search and downstream_chapters. It records human review of impacts; it does not repair a stale acceptance operation.
24
+
25
+ Document change plans accept **1–50 edits**. Their default response contains a bounded diff and edit summary; workspace output is opt-in. Both range endpoints must be individually unique. Exact matching preserves UTF-8 bytes and line endings; no automatic fuzzy selector is used by the public tool. `normalizations` reports any explicitly selected compatibility normalization. Apply/undo require the stored preview token **and `confirm=true`**. Revision is checked again under the project lock and written bytes are verified against the accepted preview.
26
+
27
+ Asset lists include revisions usable by asset update. `update_summary` replaces the synopsis; supplying a full structured body replaces its metadata too. A prose-only synopsis retains execution metadata. `promote` or `delete(cascade=false)` removes a container while preserving child chapters and their IDs. `get_meta`/`set_meta` accept a chapter ID and metadata fields `target_words`, `beats`, `hooks`, `characters`; `updates[]` reads/writes up to 50 chapters atomically under one outline revision.
28
+
29
+ ## Review scope and history
30
+
31
+ A restricted dimension request retains the complete rubric and marks unrequested criteria inconclusive. The hard gate and the minimum manuscript structure check always run. Partial reviews, insufficient coverage and unverifiable canon cannot produce delivery `pass`. Tagged model reasoning is removed; unclosed reasoning or unstructured/preamble output is rejected before manuscript generation is accepted. Chinese and English chapter headings are supported.
32
+
33
+ Uncalibrated review scores are advisory. The review v2 response omits `production_gate_status` unless the caller explicitly enables a calibrated production gate. Legacy adapters and benchmark metadata may still carry their calibration fields. `strict` records the requested review mode; it does not turn missing canon evidence into `blocked`, add another audit pass, or guarantee higher detection. A confirmed hard violation remains blocked in every mode.
34
+
35
+ `novel_review_chapter(action="versions", chapter_id=...)` lists review snapshots. `action="compare", revision=<listed SHA>` compares that snapshot to the current review. Existing current records are archived on their next save; versions overwritten by older releases cannot be reconstructed.
36
+
37
+ The supported review scope is **chapter**, returned as `check_scope="chapter"`. Book-wide repeated sentences, recurring phrase habits, quotation-style consistency and cross-chapter template detection are outside the 37-check contract. The new structure check covers headings, preamble and reasoning contamination; it is not a complete copyediting audit.
38
+
39
+ Foreshadow overdue status is **advisory**, visible through continuity/diagnostics and chapter obligations. It does not automatically block delivery. DAG cycle checks operate only on explicitly recorded dependencies; `edges: []` means no supplied dependencies, not evidence that all narrative relationships were checked. Authors must record dependencies and decide whether to delay writing for overdue promises.
40
+
41
+ ## Report coverage
42
+
43
+ | #76 item | Treatment |
44
+ |---|---|
45
+ | 1–2 | Preserve failed-commit prose; expose and resume draft handles; partial task retry |
46
+ | 3–5 | Structured outgoing errors; immediate task handle; `since`; transport/orphan diagnostics |
47
+ | 6–7 | Busy deferral without failure accounting; truthful cancellation and bounded wait |
48
+ | 8–9 | Executable acceptance recovery/abandon; distinct actual model provenance |
49
+ | 10–11 | Shared target resolver and outline validation; asymmetric length window |
50
+ | 12–15 | Unique anchors/confirmation; asset revisions; synopsis replacement; preserve children |
51
+ | 16–17 | Full rubric coverage, permanent hard/structure checks, reasoning rejection |
52
+ | 18–24 | Async write, queue retry, shared targets, ghost cleanup, run archive, compact batches, byte verification |
53
+ | 25–28 | Optional production field; inconclusive canon/coverage; explicit strict scope; review history |
54
+ | 29 | Explicit chapter scope; minimal format gate; book aggregation explicitly out of scope |
55
+ | 30 | Revision-gated field metadata, including atomic chapter batches |
56
+ | 31 | Documented advisory overdue responsibilities and explicit DAG dependency scope |
57
+ | 32 | Expanded acknowledgement domains |
58
+
59
+ ## English (#65)
60
+
61
+ Choose **Settings → General → Language → English** in dsh (`locale.preference`). The panel follows the active locale. The agent persona and every skill entry point instruct the agent to answer in the author's language, including slash-command help, plans and explanations. An explicitly requested manuscript language takes precedence for creative text. Managed runtime messages are English; Studio HTTP errors include English `message`, stable `code`, `details` and `request_id`, while retaining the original diagnostic in `error`. Task phases/errors provide English text. Existing Chinese project/source content is not translated automatically.
@@ -6,9 +6,18 @@
6
6
 
7
7
  新建作品的验收请求最多等待 60 秒(原为 15 秒),保留成功耗时和失败诊断;不重试写请求,超时仍失败。此调整容纳慢速运行器,不代表初始化耗时问题已被修复。
8
8
 
9
+ ## 0.2.19
10
+
11
+ [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.19) 已发布,直接使用[完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38167325137) 验收的同一份包。来源提交 `e0c1ab7`,npm 包 SHA-256 为 `97f89586e4c43071ebca67abd5af1df1ae42b637cf6637d4fc26ca31c133d784`。六组原生平台/Node、源码安装与 alpha 兼容共 8 份运行报告、三代宿主客户端 3 份报告全部通过;Release 四份附件的校验值已与 CI `validated-release` 产物逐一核对。npm `dsh-openwrite@0.2.19` 已发布到官方 registry(registry.npmjs.org),`latest` 已更新到 0.2.19;从官方 registry 无认证重新下载后,SHA-256 与上述 CI 包一致。
12
+
13
+ 修复 [#74](https://github.com/LiPu-jpg/Openwrite/issues/74) 与 [#75](https://github.com/LiPu-jpg/Openwrite/issues/75),均为插件侧改动,Core 保持 5.8.9(native-core `d0a75e30047c28c99af909e1b85b07190abb1fb7`),wheel 与 requirements.lock 未变。
14
+
15
+ - #74:vendor/dsh-dog 运行时不再裸导入 `@deepseek-ai/dsh-home-paths` 与 `@deepseek-ai/dsh-tools` 两个 peer 宿主包(pnpm 隔离布局下从 vendor 目录不可解析 → openwrite-dog 条目导入失败 → openwriteDog 未注册 → 预设从列表消失)。`dshHomePath` 与 `defineTool` 由新增 `src/host-shims.ts` 自包含实现,行为对齐宿主;构建产物 `lib/index.js` 只导入 node 内置模块与 dependencies 内的 `@deepseek-ai/schemastery`,独立 `import()` 成功。新增 host-shims 单元测试 10 例,dsh-dog 共 60/60 通过。
16
+ - #75:bridge 的 90 个 `novel_*` 工具统一经本地 `defineTool`(复用宿主 `parameterSchemaSpecToJsonSchema`/`valueSchemaSpecToJsonSchema`/`validateJsonSchemaValue`/`ToolArgsError`),在 schema 校验前对 `type: 'json'` 参数(含 array 的 json items)做字符串 `JSON.parse` 归一化;报错带参数名与实际类型。8 项归一化冒烟用例通过;四个宿主符号已核对在 0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2 三个受支持宿主中均导出且签名一致。
17
+
9
18
  ## 0.2.18
10
19
 
11
- <!-- TODO: 发布后回填 PR、CI、Release 链接与 SHA -->
20
+ [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.18) 已发布,直接使用[完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38059156641)验收的同一份包。来源提交 `6e84c2a`,npm 包 SHA-256 为 `edd510c6d4de58bfa09eb30f8a3b74e5a0e6cf7cf2425bc6fe3681b732c1a196`。六组原生平台/Node、源码安装、alpha 兼容及三代宿主客户端共 12 份报告全部通过;Release 四份附件的校验值已与 CI `validated-release` 产物逐一核对。合并来源 [PR #73](https://github.com/LiPu-jpg/Openwrite/pull/73)。npm `dsh-openwrite@0.2.18` 已发布到官方 registry(registry.npmjs.org),`latest` 已更新到 0.2.18;从官方 registry 无认证重新下载后,SHA-256 与上述 CI 包一致。main CI 首次运行 source-install 因 pnpm `--find-links` 目录瞬时不存在失败,重跑同一提交通过,判定为运行器 flake。
12
21
 
13
22
  修正 0.2.17 的 `auto:cheapest`:真实 Key 实测发现 OpenRouter 上部分免费模型没有 `:free` 后缀(`inclusionai/ling-3.1-flash` 定价 0/0),会被低价策略选中;免费模型可用性不稳定,Core 5.8.9(native-core `d0a75e30047c28c99af909e1b85b07190abb1fb7`)把零价模型一并排除,`auto:cheapest` 现在落在付费低价模型上。回归测试补充零价排除断言,9/9 通过。
14
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-openwrite",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "OpenWrite 长篇小说领域后端接入 DeepSeek Harness:统一创作 Agent 预设 + 原生创作工作台",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -39,6 +39,7 @@ export declare class StudioError extends Error {
39
39
  readonly status: number;
40
40
  readonly code: string;
41
41
  readonly details: JsonValue;
42
+ requestId: string | null;
42
43
  constructor(message: string, status: number, code: string, details?: JsonValue);
43
44
  }
44
45
  export interface StudioClientOptions {
@@ -1,3 +1,6 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
1
4
  /** Header OpenWrite requires on every write request (POST/PUT). */
2
5
  const WRITE_HEADER = 'X-OpenWrite-Studio';
3
6
  /** HTTP header values are bytes, so Unicode paths need explicit wire encoding.
@@ -13,6 +16,7 @@ export class StudioError extends Error {
13
16
  status;
14
17
  code;
15
18
  details;
19
+ requestId = null;
16
20
  constructor(message, status, code, details = null) {
17
21
  super(message);
18
22
  this.name = 'StudioError';
@@ -61,6 +65,8 @@ export function mutationAffectsState(path, body, headers) {
61
65
  if (declared === '0' || declared === '1')
62
66
  return declared === '1';
63
67
  const endpoint = path.split('?')[0]?.replace(/^\/+/, '').replace(/^api\//, '');
68
+ if (endpoint === 'review/history')
69
+ return false;
64
70
  if (endpoint !== 'manuscript-editing')
65
71
  return true;
66
72
  let payload = body;
@@ -180,6 +186,18 @@ export class StudioClient {
180
186
  return await fetch(target, { ...init, headers, signal: AbortSignal.any(signals) });
181
187
  }
182
188
  catch (error) {
189
+ if (this.context?.workspaceRoot) {
190
+ try {
191
+ const root = join(this.context.workspaceRoot, '.openwrite', 'transport-errors');
192
+ await mkdir(root, { recursive: true });
193
+ await writeFile(join(root, `${randomUUID()}.json`), JSON.stringify({
194
+ source: 'transport', code: signal?.aborted ? 'REQUEST_ABORTED' : 'NETWORK_ERROR',
195
+ created_at: new Date().toISOString(), path: url.pathname,
196
+ tool_name: this.context.toolName ?? '', tool_call_id: this.context.toolCallId ?? '',
197
+ }));
198
+ }
199
+ catch { /* A diagnostic failure must not hide the transport failure. */ }
200
+ }
183
201
  if (signal?.aborted)
184
202
  throw error;
185
203
  const cause = error instanceof Error ? error.cause : undefined;
@@ -203,10 +221,14 @@ export class StudioClient {
203
221
  const body = asJson(parsed);
204
222
  if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
205
223
  const error = body['error'];
206
- if (typeof error === 'string' && error.length > 0) {
207
- const code = typeof body['code'] === 'string' ? body['code'] : 'STUDIO_ERROR';
208
- const details = 'details' in body ? body['details'] : null;
209
- throw new StudioError(error, response.status, code, details);
224
+ if (error !== undefined && error !== null && error !== false && error !== '') {
225
+ const nested = typeof error === 'object' && !Array.isArray(error) ? error : {};
226
+ const code = typeof body['code'] === 'string' ? body['code'] : typeof nested['code'] === 'string' ? nested['code'] : 'STUDIO_ERROR';
227
+ const message = typeof body['message'] === 'string' ? body['message'] : typeof error === 'string' ? error : typeof nested['message'] === 'string' ? nested['message'] : JSON.stringify(error);
228
+ const details = body['details'] ?? nested['details'] ?? null;
229
+ const failure = new StudioError(message, response.status, code, details);
230
+ failure.requestId = typeof body['request_id'] === 'string' ? body['request_id'] : null;
231
+ throw failure;
210
232
  }
211
233
  // HTTP status remains authoritative even when a proxy or incompatible
212
234
  // server sends a success-shaped envelope with an error status.
@@ -6,7 +6,7 @@ export interface ReviewV2Decision {
6
6
  coverage: number;
7
7
  gate_status: "pass" | "blocked" | "inconclusive";
8
8
  delivery_status: "pass" | "revise" | "blocked" | "inconclusive" | "stale";
9
- production_gate_status: "pass" | "revise" | "blocked" | "inconclusive" | "stale" | "disabled" | "disabled_uncalibrated";
9
+ production_gate_status?: "pass" | "revise" | "blocked" | "inconclusive" | "stale" | "disabled" | "disabled_uncalibrated";
10
10
  [key: string]: unknown;
11
11
  }
12
12
  /** dsh-novel review manifest v2. Extra keys are allowed (additionalProperties). */
@@ -1466,8 +1466,7 @@ const SCHEMAS = {
1466
1466
  "quality_score",
1467
1467
  "coverage",
1468
1468
  "gate_status",
1469
- "delivery_status",
1470
- "production_gate_status"
1469
+ "delivery_status"
1471
1470
  ],
1472
1471
  "title": "OpenWrite review v2 decision",
1473
1472
  "type": "object"
@@ -41,6 +41,8 @@ const WRITABLE_PATHS = new Set([
41
41
  'manuscript/acceptance/external',
42
42
  'manuscript/acceptance/reconcile',
43
43
  'manuscript/acceptance/ack',
44
+ 'manuscript/acceptance/abandon',
45
+ 'review/history',
44
46
  'project/init',
45
47
  'project/metadata',
46
48
  'model',
@@ -46,15 +46,15 @@ export function apply(ctx, config) {
46
46
  const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)); };
47
47
  const rejection = authorizeRequest(req.headers);
48
48
  if (rejection !== undefined) {
49
- respond(rejection, { error: 'dsh browser authentication required' });
49
+ respond(rejection, { error: 'dsh browser authentication required', message: 'dsh browser authentication required', code: 'AUTHENTICATION_REQUIRED' });
50
50
  return;
51
51
  }
52
52
  if (req.method === 'GET') {
53
- respond(200, runtime?.status() ?? { phase: 'ready', message: '使用外部写作环境' });
53
+ respond(200, runtime?.status() ?? { phase: 'ready', message: 'Using an external writing environment' });
54
54
  return;
55
55
  }
56
56
  if (req.method !== 'POST' || req.headers['x-openwrite-studio'] !== '1') {
57
- respond(405, { error: '操作不允许' });
57
+ respond(405, { error: 'Method not allowed', message: 'Method not allowed', code: 'METHOD_NOT_ALLOWED' });
58
58
  return;
59
59
  }
60
60
  const chunks = [];
@@ -62,7 +62,7 @@ export function apply(ctx, config) {
62
62
  for await (const chunk of req) {
63
63
  size += chunk.length;
64
64
  if (size > 1024) {
65
- respond(413, { error: '请求过大' });
65
+ respond(413, { error: 'Request too large', message: 'Request too large', code: 'REQUEST_TOO_LARGE' });
66
66
  return;
67
67
  }
68
68
  ;
@@ -75,13 +75,13 @@ export function apply(ctx, config) {
75
75
  else if (action === 'prepare' || action === 'retry')
76
76
  void runtime?.ensure().catch(() => { });
77
77
  else {
78
- respond(400, { error: '未知操作' });
78
+ respond(400, { error: 'Unknown action', message: 'Unknown action', code: 'INVALID_ACTION' });
79
79
  return;
80
80
  }
81
81
  respond(202, runtime?.status() ?? { phase: 'ready' });
82
82
  }
83
83
  catch {
84
- respond(400, { error: '请求无效' });
84
+ respond(400, { error: 'Invalid request', message: 'Invalid request', code: 'INVALID_INPUT' });
85
85
  }
86
86
  },
87
87
  }), 'openwrite: runtime controls'));
@@ -147,7 +147,7 @@ export async function stopOwnedProcess(child) {
147
147
  export class ManagedRuntime {
148
148
  root;
149
149
  artifacts;
150
- state = { phase: 'idle', message: '首次使用将准备写作环境' };
150
+ state = { phase: 'idle', message: 'The writing environment will be prepared on first use' };
151
151
  pending;
152
152
  controller;
153
153
  child;
@@ -175,7 +175,7 @@ export class ManagedRuntime {
175
175
  }
176
176
  ensure() {
177
177
  if (this.closed)
178
- return Promise.reject(new Error('OpenWrite 已卸载'));
178
+ return Promise.reject(new Error('OpenWrite was removed'));
179
179
  if (this.connection)
180
180
  return this.verifyConnection(this.connection);
181
181
  if (this.pending)
@@ -189,7 +189,7 @@ export class ManagedRuntime {
189
189
  this.controller = controller;
190
190
  this.pending = this.prepare(controller.signal).catch(error => {
191
191
  this.state = { phase: controller.signal.aborted ? 'cancelled' : 'error',
192
- message: controller.signal.aborted ? '准备已取消,可重试' : '写作环境未就绪,可重试',
192
+ message: controller.signal.aborted ? 'Setup was cancelled; retry when ready' : 'The writing environment is not ready; retry setup',
193
193
  error: controller.signal.aborted ? undefined : sanitizeDiagnostic(String(error instanceof Error ? error.message : error)) };
194
194
  throw error;
195
195
  }).finally(() => { this.pending = undefined; this.controller = undefined; });
@@ -205,7 +205,7 @@ export class ManagedRuntime {
205
205
  async verifyConnection(connection) {
206
206
  if (await this.probeHealth(connection)) {
207
207
  if (this.closed)
208
- throw new Error('OpenWrite 已卸载');
208
+ throw new Error('OpenWrite was removed');
209
209
  const current = this.connection;
210
210
  if (current)
211
211
  return current;
@@ -219,10 +219,10 @@ export class ManagedRuntime {
219
219
  if (child)
220
220
  await stopOwnedProcess(child).catch(() => { });
221
221
  if (this.closed)
222
- throw new Error('OpenWrite 已卸载');
222
+ throw new Error('OpenWrite was removed');
223
223
  if (!this.allowRevive)
224
- throw new Error('准备已取消,可重试');
225
- this.update('recovering', '写作后端无响应,正在恢复');
224
+ throw new Error('Setup was cancelled; retry when ready');
225
+ this.update('recovering', 'The writing backend is unresponsive; recovering');
226
226
  return this.queueRestart();
227
227
  }
228
228
  /** Short-timeout liveness probe; false covers exit, refusal, hang, and auth drift. */
@@ -257,7 +257,7 @@ export class ManagedRuntime {
257
257
  this.child = undefined;
258
258
  }
259
259
  this.connection = undefined;
260
- this.update('uninstalled', 'OpenWrite 已卸载,作品和配置已保留');
260
+ this.update('uninstalled', 'OpenWrite was removed; manuscripts and settings were preserved');
261
261
  }
262
262
  exitDiagnostic(code, signalName, stderr = '') {
263
263
  return sanitizeDiagnostic(`exit ${code ?? 'none'} signal ${signalName ?? 'none'}${stderr ? `: ${stderr}` : ''}`);
@@ -272,17 +272,17 @@ export class ManagedRuntime {
272
272
  return;
273
273
  const error = this.exitDiagnostic(code, signalName, stderr);
274
274
  if (!this.allowRevive) {
275
- this.update('cancelled', '准备已取消,可重试', { error });
275
+ this.update('cancelled', 'Setup was cancelled; retry when ready', { error });
276
276
  return;
277
277
  }
278
278
  this.restartAttempts += 1;
279
279
  if (this.restartAttempts > this.restartLimit) {
280
280
  this.allowRevive = false;
281
- this.update('error', '写作后端多次异常退出,已停止自动恢复,可手动重试', { error });
281
+ this.update('error', 'The writing backend exited repeatedly; automatic recovery stopped. Retry manually', { error });
282
282
  return;
283
283
  }
284
284
  this.recovered = true;
285
- this.update('recovering', `写作后端异常退出,正在恢复(${this.restartAttempts}/${this.restartLimit})`, { error });
285
+ this.update('recovering', `The writing backend exited; recovering (${this.restartAttempts}/${this.restartLimit})`, { error });
286
286
  // Auto-recovery has no caller; never let a rejected prepare become an unhandled rejection that kills dsh.
287
287
  void this.queueRestart().catch(() => { });
288
288
  }
@@ -293,11 +293,11 @@ export class ManagedRuntime {
293
293
  let assigned;
294
294
  const chained = prior.then(connection => {
295
295
  if (this.closed)
296
- throw new Error('OpenWrite 已卸载');
296
+ throw new Error('OpenWrite was removed');
297
297
  if (this.live())
298
298
  return connection;
299
299
  if (!this.allowRevive)
300
- throw new Error('准备已取消,可重试');
300
+ throw new Error('Setup was cancelled; retry when ready');
301
301
  if (this.pending === assigned)
302
302
  this.pending = undefined;
303
303
  return this.queueRestart();
@@ -347,16 +347,16 @@ export class ManagedRuntime {
347
347
  const diagnostic = sanitizeDiagnostic(String(error instanceof Error ? error.message : error));
348
348
  if (this.restartAttempts > this.restartLimit) {
349
349
  this.allowRevive = false;
350
- this.update('error', '写作后端多次异常退出,已停止自动恢复,可手动重试', { error: diagnostic });
350
+ this.update('error', 'The writing backend exited repeatedly; automatic recovery stopped. Retry manually', { error: diagnostic });
351
351
  throw error;
352
352
  }
353
- this.update('recovering', `写作后端异常退出,正在恢复(${this.restartAttempts}/${this.restartLimit})`, { error: diagnostic });
353
+ this.update('recovering', `The writing backend exited; recovering (${this.restartAttempts}/${this.restartLimit})`, { error: diagnostic });
354
354
  }
355
355
  }
356
356
  if (this.closed)
357
- throw new Error('OpenWrite 已卸载');
358
- this.update('cancelled', '准备已取消,可重试');
359
- throw lastError instanceof Error ? lastError : new Error('准备已取消,可重试');
357
+ throw new Error('OpenWrite was removed');
358
+ this.update('cancelled', 'Setup was cancelled; retry when ready');
359
+ throw lastError instanceof Error ? lastError : new Error('Setup was cancelled; retry when ready');
360
360
  }
361
361
  async download(item, signal) {
362
362
  const cache = join(this.root, 'cache');
@@ -365,11 +365,11 @@ export class ManagedRuntime {
365
365
  if (await sha256(destination).catch(() => '') === item.sha256)
366
366
  return destination;
367
367
  const partial = destination + '.' + randomUUID() + '.partial';
368
- this.update('downloading', '正在下载并校验写作环境');
368
+ this.update('downloading', 'Downloading and verifying the writing environment');
369
369
  try {
370
370
  const response = await fetch(item.url, { signal: AbortSignal.any([signal, AbortSignal.timeout(600_000)]) });
371
371
  if (!response.ok || !response.body)
372
- throw new Error(`下载失败:HTTP ${response.status}`);
372
+ throw new Error(`Download failed: HTTP ${response.status}`);
373
373
  const size = Number(response.headers.get('content-length')) || undefined;
374
374
  const file = await open(partial, 'wx', 0o600);
375
375
  let received = 0;
@@ -378,16 +378,16 @@ export class ManagedRuntime {
378
378
  signal.throwIfAborted();
379
379
  received += chunk.length;
380
380
  if (received > 1024 ** 3)
381
- throw new Error('下载超过允许大小');
381
+ throw new Error('Download exceeds the allowed size');
382
382
  await file.write(chunk);
383
- this.state = { phase: 'downloading', message: '正在下载并校验写作环境', downloadedBytes: received, totalBytes: size };
383
+ this.state = { phase: 'downloading', message: 'Downloading and verifying the writing environment', downloadedBytes: received, totalBytes: size };
384
384
  }
385
385
  }
386
386
  finally {
387
387
  await file.close();
388
388
  }
389
389
  if (await sha256(partial) !== item.sha256)
390
- throw new Error('下载校验失败,请重试');
390
+ throw new Error('Download checksum failed; retry');
391
391
  await rename(partial, destination);
392
392
  return destination;
393
393
  }
@@ -408,7 +408,7 @@ export class ManagedRuntime {
408
408
  for (const [name, bytes] of Object.entries(unzipSync(await readFile(archive)))) {
409
409
  const target = resolve(dir, name);
410
410
  if (isAbsolute(name) || !target.startsWith(dir + sep))
411
- throw new Error('压缩包路径无效');
411
+ throw new Error('Invalid archive path');
412
412
  if (name.endsWith('/'))
413
413
  await mkdir(target, { recursive: true });
414
414
  else {
@@ -437,7 +437,7 @@ export class ManagedRuntime {
437
437
  try {
438
438
  await new Promise((done, reject) => {
439
439
  child.once('error', reject);
440
- child.once('exit', code => code === 0 ? done() : reject(new Error(`环境准备失败 (${code}):${failure}`)));
440
+ child.once('exit', code => code === 0 ? done() : reject(new Error(`Environment setup failed (${code}):${failure}`)));
441
441
  });
442
442
  signal.throwIfAborted();
443
443
  }
@@ -457,7 +457,7 @@ export class ManagedRuntime {
457
457
  catch (error) {
458
458
  if (error.code !== 'ELOCKED')
459
459
  throw error;
460
- this.update('waiting', '另一个 OpenWrite 正在准备环境,等待完成');
460
+ this.update('waiting', 'Another OpenWrite instance is preparing the environment; waiting');
461
461
  await this.wait(250, undefined, { signal });
462
462
  }
463
463
  }
@@ -466,17 +466,17 @@ export class ManagedRuntime {
466
466
  const manifest = JSON.parse(await readFile(join(this.artifacts, 'runtime-manifest.json'), 'utf8'));
467
467
  const platform = manifest.platforms[`${process.platform}-${process.arch}`];
468
468
  if (!platform)
469
- throw new Error(`暂不支持 ${process.platform}/${process.arch}`);
469
+ throw new Error(`Unsupported platform: ${process.platform}/${process.arch}`);
470
470
  if (!manifest.wheel || !manifest.requirements)
471
- throw new Error('发布包缺少 Core 或依赖校验清单');
471
+ throw new Error('The package is missing Core or dependency checksums');
472
472
  const wheel = join(this.artifacts, manifest.wheel.file);
473
473
  const requirements = join(this.artifacts, manifest.requirements.file);
474
474
  if (await sha256(wheel) !== manifest.wheel.sha256 || await sha256(requirements) !== manifest.requirements.sha256) {
475
- throw new Error('发布包校验失败,请重新安装');
475
+ throw new Error('Package verification failed; reinstall OpenWrite');
476
476
  }
477
477
  for (const artifact of manifest.dependency_wheels ?? []) {
478
478
  if (await sha256(join(this.artifacts, artifact.file)) !== artifact.sha256)
479
- throw new Error('平台依赖校验失败,请重新安装');
479
+ throw new Error('Platform dependency verification failed; reinstall OpenWrite');
480
480
  }
481
481
  await mkdir(this.root, { recursive: true, mode: 0o700 });
482
482
  const unlock = await this.lock(signal);
@@ -488,7 +488,7 @@ export class ManagedRuntime {
488
488
  if (await readFile(join(destination, '.complete'), 'utf8').catch(() => '') !== generation) {
489
489
  const uv = await this.extract(platform.uv, signal);
490
490
  const basePython = await this.extract(platform.python, signal);
491
- this.update('installing', '正在安装隔离依赖,首次使用可能需要几分钟');
491
+ this.update('installing', 'Installing isolated dependencies; first use may take several minutes');
492
492
  await rm(destination, { recursive: true, force: true });
493
493
  try {
494
494
  await this.command(uv, ['venv', '--python', basePython, destination], signal);
@@ -504,7 +504,7 @@ export class ManagedRuntime {
504
504
  }
505
505
  }
506
506
  signal.throwIfAborted();
507
- this.update(this.recovered ? 'recovering' : 'starting', this.recovered ? '正在恢复写作环境' : '正在启动写作环境');
507
+ this.update(this.recovered ? 'recovering' : 'starting', this.recovered ? 'Recovering the writing environment' : 'Starting the writing environment');
508
508
  const connection = await this.start(python, manifest, signal);
509
509
  const pending = join(this.root, 'active.' + randomUUID() + '.json');
510
510
  try {
@@ -562,9 +562,9 @@ export class ManagedRuntime {
562
562
  child.once('exit', (code, signalName) => {
563
563
  if (started)
564
564
  return;
565
- reject(new Error(`写作后端在启动时退出 (${code ?? 'none'}/${signalName ?? 'none'})`));
565
+ reject(new Error(`The writing backend exited during startup (${code ?? 'none'}/${signalName ?? 'none'})`));
566
566
  });
567
- timer = setTimeout(() => reject(new Error('写作后端启动超时')), 120_000);
567
+ timer = setTimeout(() => reject(new Error('Writing backend startup timed out')), 120_000);
568
568
  lines.on('line', line => {
569
569
  try {
570
570
  const message = JSON.parse(line);
@@ -591,15 +591,15 @@ export class ManagedRuntime {
591
591
  });
592
592
  const result = await health.json();
593
593
  if (!health.ok || result.core_version !== manifest.core_version || result.contract_version !== manifest.contract_version) {
594
- throw new Error('写作后端版本或接口不匹配,请重新安装');
594
+ throw new Error('Writing backend version or API mismatch; reinstall OpenWrite');
595
595
  }
596
596
  this.connection = connection;
597
597
  started = true;
598
598
  if (!this.recovered)
599
599
  this.restartAttempts = 0;
600
600
  this.update('ready', this.recovered
601
- ? '写作环境已恢复;进行中的写作或评审任务不会自动重试'
602
- : '写作环境已就绪');
601
+ ? 'The writing environment recovered; in-progress writing and review tasks require an explicit retry'
602
+ : 'The writing environment is ready');
603
603
  return connection;
604
604
  }
605
605
  catch (error) {