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.
- package/README.md +22 -5
- package/docs/ISSUE_76_REPAIRS.md +61 -0
- package/docs/RELEASE_ACCEPTANCE.md +10 -1
- package/package.json +1 -1
- package/packages/openwrite-bridge/lib/client.d.ts +1 -0
- package/packages/openwrite-bridge/lib/client.js +26 -4
- package/packages/openwrite-bridge/lib/contracts-generated.d.ts +1 -1
- package/packages/openwrite-bridge/lib/contracts-generated.js +1 -2
- package/packages/openwrite-bridge/lib/domain.js +2 -0
- package/packages/openwrite-bridge/lib/index.js +6 -6
- package/packages/openwrite-bridge/lib/managed-runtime.js +37 -37
- package/packages/openwrite-bridge/lib/tools.js +198 -32
- package/packages/studio-panel/lib/client.js +488 -488
- package/presets/openwrite/agent.cordis.yml +10 -2
- package/presets/openwrite/skills/canon/SKILL.md +2 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-import-query/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-review-query/SKILL.md +2 -0
- package/presets/openwrite/skills/export-book/SKILL.md +2 -0
- package/presets/openwrite/skills/foreshadow/SKILL.md +2 -0
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +2 -0
- package/presets/openwrite/skills/learn/SKILL.md +2 -0
- package/presets/openwrite/skills/novel-creator/SKILL.md +2 -0
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-deslop/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-analyze/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-scan/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-write/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-review/SKILL.md +2 -0
- package/presets/openwrite/skills/progress/SKILL.md +2 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -0
- package/presets/openwrite/skills/revise-span/SKILL.md +2 -0
- package/presets/openwrite/skills/style-system/SKILL.md +2 -0
- package/presets/openwrite/skills/truth-validation/SKILL.md +2 -0
- package/presets/openwrite/skills/workflow-manager/SKILL.md +2 -0
- package/presets/openwrite/skills/world-query/SKILL.md +2 -0
- package/presets/openwrite/skills/write-next/SKILL.md +2 -0
- package/release/{openwrite-5.8.9-py3-none-any.whl → openwrite-5.8.10-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +9 -6
- package/release/source.json +3 -3
- package/vendor/dsh-dog/lib/core.js +1 -1
- package/vendor/dsh-dog/lib/index.d.ts +1 -1
- package/vendor/dsh-dog/lib/index.js +202 -4
- 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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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 (
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
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
|
|
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). */
|
|
@@ -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', '
|
|
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', '
|
|
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',
|
|
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', '
|
|
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',
|
|
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(
|
|
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: '
|
|
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(
|
|
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', '
|
|
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(
|
|
469
|
+
throw new Error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
470
470
|
if (!manifest.wheel || !manifest.requirements)
|
|
471
|
-
throw new Error('
|
|
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(
|
|
565
|
+
reject(new Error(`The writing backend exited during startup (${code ?? 'none'}/${signalName ?? 'none'})`));
|
|
566
566
|
});
|
|
567
|
-
timer = setTimeout(() => reject(new Error('
|
|
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) {
|