dsh-openwrite 0.2.19 → 0.2.21
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 +23 -6
- package/docs/INSTALL.md +6 -0
- package/docs/ISSUE_76_REPAIRS.md +61 -0
- package/docs/RELEASE_ACCEPTANCE.md +14 -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 +67 -31
- package/packages/studio-panel/lib/client.js +493 -484
- package/packages/studio-panel/lib/client.js.map +1 -1
- 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 +5 -5
- package/release/source.json +3 -3
package/README.md
CHANGED
|
@@ -29,14 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
## 快速安装
|
|
31
31
|
|
|
32
|
-
**0.2.
|
|
32
|
+
**0.2.21 标准插件包**兼容精确 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.21/dsh-openwrite-0.2.21.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.21` 修复 [#79](https://github.com/LiPu-jpg/Openwrite/issues/79) 的 Windows Desktop 目录选择:宿主接口失败后使用桌面提供的系统文件夹对话框,覆盖侧栏进入与工作台内目录选择。Core 保持 5.8.10。排查与手填路径方式见 [安装指南](docs/INSTALL.md#windows-desktop-目录选择)。
|
|
68
|
+
|
|
69
|
+
`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 为准。
|
|
70
|
+
|
|
69
71
|
`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
72
|
|
|
71
73
|
`0.2.18` 候选包修正 `auto:cheapest`(Core 5.8.9):零价模型不再参与比价,自动落到付费低价模型上。正式可下载版本以 npm `latest` 和 GitHub Release 为准。升级后需停止并重新启动 dsh,再刷新浏览器页面。
|
|
@@ -84,7 +86,7 @@ dsh web
|
|
|
84
86
|
|
|
85
87
|
`0.2.11` 修复 [#45](https://github.com/LiPu-jpg/Openwrite/issues/45),适配精确宿主版本 `0.2.0-rc.2`:工作台兼容新的 turn-tail list 槽位,DoG 调试面板使用新的 `sessionStatus`;根包和所有子包的兼容声明同步更新。已验证 0.2.0-rc.2 的 Web 客户端启动、OpenWrite 入口、DoG 面板及其他插件共存;Windows Electron 桌面端恢复链路尚未实机复测。
|
|
86
88
|
|
|
87
|
-
**`dsh-openwrite@0.2.10` 不包含这项修复,请升级到 `0.2.
|
|
89
|
+
**`dsh-openwrite@0.2.10` 不包含这项修复,请升级到 `0.2.21` 的 GitHub Release 包。** 不要通过给旧子包授权来代替升级:`dsh-openwrite` 的版本豁免不会覆盖 `@dsh-novel/studio-panel@<ver>` 和 `@dsh-external/dsh-dog@<ver>`,而旧客户端在 0.2.0-rc.2 上激活失败可能触发桌面端的 profile 恢复。
|
|
88
90
|
|
|
89
91
|
若已发生恢复,先关闭桌面端并备份当前 profile,再从自己的 `cordis.patch.yml.bak-*` 备份恢复 patch,同时恢复原 `package.json` 的 `dsh.profile.bundles` 列表。安装包和作品目录通常仍在;不要用出厂模板覆盖其他插件配置。详细排查见 [安装指南](docs/INSTALL.md#dsh-02-客户端兼容排查)。
|
|
90
92
|
|
|
@@ -103,7 +105,7 @@ dsh web
|
|
|
103
105
|
|
|
104
106
|
已有会话不会自动切换预设。详细操作与工作区规则见 [使用流程](docs/WORKFLOWS.md)。
|
|
105
107
|
|
|
106
|
-
**界面语言**:面板已内置中文与英文词典,跟随 dsh 语言设置。切换方法:Settings → General → Language → English(选择后存为 `locale.preference
|
|
108
|
+
**界面语言**:面板已内置中文与英文词典,跟随 dsh 语言设置。切换方法:Settings → General → Language → English(选择后存为 `locale.preference`)。Agent 的 persona 和所有技能入口要求用作者的对话语言回复;运行环境与任务状态提供英文,Studio 报错附带英文说明和稳定错误码。已有作品内容不会自动翻译,正文按作者指定的语言生成。
|
|
107
109
|
|
|
108
110
|
## 配置模型
|
|
109
111
|
|
|
@@ -167,3 +169,18 @@ conductor/ 连续写章、评审与修订编排
|
|
|
167
169
|
Logo 基于 OpenWrite `native-core` 分支,已调整深浅主题配色;基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),图谱集成使用 [dsh-dog](https://github.com/Fun10165/dsh-dog)。
|
|
168
170
|
|
|
169
171
|
生态目录:[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) · [社区插件市场](https://dshtui.com/plugins/)。OpenWrite 当前面向 DeepSeek Harness 的 `web` profile,使用 `dsh web` 打开创作工作台;尚未验证 dsh-TUI 界面兼容性。
|
|
172
|
+
|
|
173
|
+
## Language / 语言
|
|
174
|
+
|
|
175
|
+
In dsh, choose **Settings → General → Language → English** (`locale.preference`).
|
|
176
|
+
The OpenWrite panel follows the host language. The writing agent and slash-command
|
|
177
|
+
help follow your conversation language; manuscripts, outlines and assets retain
|
|
178
|
+
their existing language. You can say “Please write and review this novel in English.”
|
|
179
|
+
The bundled Chinese skill files are procedural instructions, not an output-language
|
|
180
|
+
restriction. Runtime setup messages use English. Backend failures include stable
|
|
181
|
+
`code`, English `message`, `request_id`, and the original diagnostic text.
|
|
182
|
+
|
|
183
|
+
在 dsh 的 **设置 → 通用 → 语言 → English** 切换面板语言。Agent 的回复和命令帮助跟随
|
|
184
|
+
当前对话语言;正文与资产遵循作品已有语言,除非作者要求翻译。
|
|
185
|
+
|
|
186
|
+
任务恢复、编辑确认及审稿职责范围见 [可靠性修复说明](docs/ISSUE_76_REPAIRS.md)。
|
package/docs/INSTALL.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
安装已发布的版本;可下载产物、SHA-256 和实际平台结果以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准。验收方法见 [验收报告](RELEASE_ACCEPTANCE.md)。
|
|
4
4
|
|
|
5
|
+
## Windows Desktop 目录选择
|
|
6
|
+
|
|
7
|
+
若留空作品目录并点击“选择目录并进入”时出现 `directoryPicker.pick needs the native capability; the composed picker serves "browse"`,这是桌面宿主组合的目录选择能力与其接口要求不一致。OpenWrite 0.2.21 在宿主接口失败后调用桌面公开的 `__DSH_DESKTOP_PICK_DIRECTORY__`,覆盖侧栏进入、作品绑定、新建与恢复目录选择。取消目录对话框不会创建作品或会话;手填绝对路径仍可直接进入。
|
|
8
|
+
|
|
9
|
+
没有桌面选择通道的宿主会保留原始错误。此时可在同一个输入框填写作品的完整绝对路径,例如 `A:\AI\我的小说`,点击“进入作品”。升级插件后需要停止并重新启动 dsh,使客户端重新加载。
|
|
10
|
+
|
|
5
11
|
## 标准安装
|
|
6
12
|
|
|
7
13
|
本次锁定 dsh `0.1.2-rc.1`,Node 24 为完整验收基线;Node 22.19 / 26 做兼容检查。安装不自动升级 dsh。先执行 `dsh --version` 核对宿主。
|
|
@@ -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,22 @@
|
|
|
6
6
|
|
|
7
7
|
新建作品的验收请求最多等待 60 秒(原为 15 秒),保留成功耗时和失败诊断;不重试写请求,超时仍失败。此调整容纳慢速运行器,不代表初始化耗时问题已被修复。
|
|
8
8
|
|
|
9
|
+
## 0.2.20
|
|
10
|
+
|
|
11
|
+
[GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.20) 与 npm `dsh-openwrite@0.2.20` 已发布,npm `latest` 已更新到 0.2.20。使用[主分支完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38296670387) 验收的同一份原始产物,来源提交 `4d54f3c619f853a4243cd60e67ef6c8ba8d336af`,npm 包 SHA-256 为 `486c20fbd36909e32cc6cac1fd1e734872abec53da749c0860f8e5e1d4cf6cda`。六组原生平台/Node、源码安装与 alpha 兼容共 8 份运行报告、三代宿主客户端 3 份报告全部通过;GitHub 四份附件逐一与 CI 原文件比对一致,从官方 npm registry 无认证重新下载后,包字节及 SHA-256 也一致。
|
|
12
|
+
|
|
13
|
+
修复 [#76](https://github.com/LiPu-jpg/Openwrite/issues/76) 与 [#65](https://github.com/LiPu-jpg/Openwrite/issues/65),合并来源为引擎 [PR #77](https://github.com/LiPu-jpg/Openwrite/pull/77) 和插件 [PR #78](https://github.com/LiPu-jpg/Openwrite/pull/78)。捆绑 Core 5.8.10,wheel 来源提交 `18a500015f5d5da00f2f4d5ccd2325b084fed24b` 已包含在合并后的 native-core 分支;wheel SHA-256 为 `f121410300c7e7c55d594d0b8d7f4da41f8a17f0dfb5dc3b195e447b618bfd4d`。
|
|
14
|
+
|
|
15
|
+
- 生成后提交失败保留正文与恢复句柄;恢复提交复用已有草稿。写章返回持久化 task_id,忙碌延后、取消请求与终态分离,传输错误可追踪。
|
|
16
|
+
- 统一字数目标,补齐验收恢复、批量与字段级编辑、精确文本/换行保护、完整审稿覆盖与硬检查、正文结构及推理污染检查、审稿历史。
|
|
17
|
+
- persona 和所有技能入口跟随作者语言;运行环境与任务状态提供英文,Studio 错误附带英文说明和错误码。
|
|
18
|
+
- Core 全套 pytest 1397 passed / 32 skipped,专项回归 21 passed。本地 macOS arm64 真实候选包安装、生命周期及浏览器验收通过;正式包以以上主分支 CI 报告为准。真实模型写作未实测,不计为通过。
|
|
19
|
+
|
|
20
|
+
恢复步骤与明确的范围见 [修复说明](ISSUE_76_REPAIRS.md)。跨章聚合与逾期伏笔硬阻断按 issue 允许的方案声明职责范围,本版未新增这些功能。
|
|
21
|
+
|
|
9
22
|
## 0.2.19
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
[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
25
|
|
|
13
26
|
修复 [#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
27
|
|
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) {
|