@ckanner/dsh-plugin-flywheel 0.28.1 → 0.28.3

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/PROTOCOL.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Go 在工作目录启动 `flywheel-dsh-worker`,写入**一条** UTF-8 JSON 行并关闭 stdin。关闭 stdin 表示请求已完整发送,不表示取消任务。单条输入及 SDK 帧上限均为 8 MiB。stdout 只输出 JSONL,诊断走 stderr。
8
8
 
9
9
  ```json
10
- {"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","provider":"deepseek-official","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000,"maxTokens":32768}
10
+ {"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","provider":"deepseek-account","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000,"maxTokens":32768}
11
11
  ```
12
12
 
13
13
  必填字段:`type`、`protocol`、`requestId`、`cwd`、`provider`、`model`、`permissionMode`、`prompt`。`cwd` 必须是存在的绝对目录,worker 规范化为真实路径;Go 在比较前同样规范化。可选字段:
@@ -18,7 +18,9 @@ Go 在工作目录启动 `flywheel-dsh-worker`,写入**一条** UTF-8 JSON 行
18
18
  | `maxTokens` | 正整数,每次原生模型请求的输出上限,传入 SDK initialize |
19
19
  | `timeoutMs` | 1–86,400,000,默认 1,800,000;涵盖权限准备、初始化、运行和关闭 |
20
20
 
21
- `permissionMode` 仅允许 `read-only`、`workspace-write`。未知字段一律拒绝。v1 不接受 `sessionId` / resume、附件、任意额外 CLI 参数和额外写目录;不进行隐式冷启动替代恢复。
21
+ `permissionMode` 允许 `read-only`、`workspace-write` 和显式 `danger-full-access`。未知字段一律拒绝。v1 不接受 `sessionId` / resume、附件、任意额外 CLI 参数和额外写目录;不进行隐式冷启动替代恢复。
22
+
23
+ `provider` 明确选择认证路径:`deepseek-account` 使用 DSH 账号服务,`deepseek-official` 使用 API Key;不省略 provider 来猜桌面默认,worker 不在账号失败时自行换用 API。默认预设没有后备渠道,Go 仍可按用户显式配置的后备链执行原有策略。标准 SDK 与桌面在相同 DSH home 下默认共用账号授权存储,无需复制凭证。
22
24
 
23
25
  ## 运行参数
24
26
 
@@ -37,21 +39,23 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
37
39
 
38
40
  每帧均含 `protocol:1`、原 `requestId`。顺序是 `ready` → 零或多个 `event` → 恰好一个 `result`;启动失败允许直接 `result`。创建 Session 时的权限事件会缓冲至 ready 之后。
39
41
 
40
- ### ready:真实能力握手已通过
42
+ ### ready:SDK 配置与权限握手已通过
41
43
 
42
44
  ```json
43
- {"protocol":1,"requestId":"call-123","type":"ready","sessionId":"flywheel-uuid","cwd":"/absolute/worktree","provider":"deepseek-official","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","approvalPolicy":"never"}
45
+ {"protocol":1,"requestId":"call-123","type":"ready","sessionId":"flywheel-uuid","cwd":"/absolute/worktree","provider":"deepseek-account","model":"deepseek-flash","reasoningEffort":"high","permissionMode":"workspace-write","approvalPolicy":"never"}
44
46
  ```
45
47
 
46
48
  发出 ready 前必须依次满足:
47
49
 
48
50
  1. 官方 SDK `initialize` 返回 `deepseek-harness-sdk-runtime`,adapter 接受指定 provider/model/effort。
49
- 2. worker 策略插件经真实 `ctx.shell` 执行本地无副作用的 `:` 探针;退出码为 0 且报告 `sandbox.enforcement:full`。不可用或部分实施的沙箱拒绝通过。
51
+ 2. worker 策略插件经真实 `ctx.shell` 执行本地无副作用的 `:` 探针;退出码为 0、模式匹配且未被拒绝。受限模式还须报告 `sandbox.enforcement:full`,不可用或部分实施的沙箱拒绝通过;明确完全权限时记录 `confinedShell:false` 并省略 enforcement,不伪造 OS 强制。
50
52
  3. `session/working-directory/get` 创建全新的 Session,返回实际 cwd。
51
53
  4. Session 的 `sandbox/mode`、`approval/policy` 被固定,`ctx.sandboxPolicy.resolve()` 与 `ctx.approval.effectivePolicy()` 的实际读值形成 `flywheel/policy` 事件。
52
- 5. worker 比对 policy 事件的 cwd、mode、`never` 和 full confinement。
54
+ 5. worker 比对 policy 事件的 cwd、mode、`never` 及对应权限证据:受限模式为完整强制,完全权限为明确无 OS 限制。
55
+
56
+ 任一项缺失,不发送 `session/prompt`。`never` 控制交互审批,不单独扩大访问范围;必须同时显式选择 `danger-full-access` 才请求完全权限。既有静态拒绝或外部权限不足仍按真实失败处理。
53
57
 
54
- 任一项缺失,不发送 `session/prompt`。`never` 在 rc.5 的含义是自动拒绝需要批准的提权操作;它不会授予无限权限。
58
+ `initialize` 返回 serverInfo,并通过 SDK 配置校验。ready 中的 provider/model/reasoningEffort 是配置被接受后的 worker 请求回显;尤其 reasoningEffort 不是模型服务独立返回的实际强度证明,也不证明账号额度或回答质量。
55
59
 
56
60
  ### event:可审计的原始持久事件
57
61
 
@@ -71,6 +75,10 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
71
75
 
72
76
  `status` 是 `ok`、`error` 或 `cancelled`。失败提供 `error:{code,message}`;code 仅使用 Go 已有分类。`settled` 只描述 SDK 关闭证据;Go watchdog 对整个拥有的进程组做最终判定。强制终止 SDK 或未获得正常 shutdown 确认时,不能返回成功,也不能声称其原生托管进程已全部清理。
73
77
 
78
+ DeepSeek 账号的 `ACCOUNT_SIGN_IN_REQUIRED`/`ACCOUNT_TOKEN_INVALID` 精确映射到 AUTH,`ACCOUNT_QUOTA` 映射到 QUOTA。`turn/end` 的 aborted 只在其 reason 为 `{kind:"hook",reason:"deepseek-account/signed-out"}` 时同归 AUTH。保留原始诊断与恢复同一账号的提示;未知 ACCOUNT 前缀、其他 hook 和普通 JSON-RPC 传输失败沿用原分类。worker 自身取消继续优先输出 cancelled,不把取消冒充认证失败。
79
+
80
+ Go 在验证协议身份、终态、已知 code 及 error 对应物理退出码 1 后独立保留结构化错误码,编排与 doctor 不从原始消息重新猜测该码。矛盾的退出码、进程超时、spawn/signal、截断、取消和未确认清理优先处理;这些事实不因收到 AUTH/QUOTA 而消失。合法 error 帧的 settled:false 记录 SDK 清理未确认并阻止自动重试;已启动 worker 缺少可信终态回执(包括身份损坏、缺失终态或坏尾帧)同样记录 CleanupError 并停止本次运行,外层进程已退出不代替 SDK shutdown 证明;worker 启动前拒绝且 settled:true 不等于清理未知。未知码或坏协议仍属于传输/协议失败,不能借错误正文冒充可信 code。Node 也要把 SDK 关闭期间的坏帧及 EOF 未换行残帧记为 protocolFailure,并覆盖先前账户错误;外部取消继续保留 cancelled 状态。
81
+
74
82
  使用量来自**每条唯一 `assistant/message` 的已提交 per-call usage**,跨该次 worker 调用累加:
75
83
 
76
84
  - DSH 的 `inputTokens` 是未命中缓存的输入;bridge 的 `input_tokens` 是它加已报告 cache read/write 的总输入。Go 不再重复加缓存。
@@ -99,7 +107,7 @@ Go 的 `DSHArgs.Doctor` 使用这个入口;为保持 v1 必填 `prompt` 的 ad
99
107
  {"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","model":"gpt-6.1-sol","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000}
100
108
  ```
101
109
 
102
- 只接受这些字段;不接受 provider、resume、threadId、额外写目录、任意 CLI 参数或危险权限模式。requestId 非空且最多 256 字符;cwd 为绝对路径,Go 先规范化成物理目录。model/reasoningEffort/prompt 必须是非空文本,permissionMode 仅 read-only/workspace-write。timeoutMs 显式值必须为 1–2,147,483,647 的安全整数,省略使用上游 600,000 毫秒;worker 不接受通过 0 关闭期限。Go 传递有效 wall deadline 和 high 默认 thinking;worker 将 reasoningEffort 映射到 machine 的 effort,固定 threadMode=ephemeral、command=`FLYWHEEL_CODEX_BIN` 或 `codex`,不恢复旧线程。
110
+ 只接受这些字段;不接受 provider、resume、threadId、额外写目录、任意 CLI 参数或未知权限模式。requestId 非空且最多 256 字符;cwd 为绝对路径,Go 先规范化成物理目录。model/reasoningEffort/prompt 必须是非空文本,permissionMode 为 read-only/workspace-write/danger-full-access;最后一项在 Node adapter 边界映射为 machine API 的 `dangerously-bypass-approvals-and-sandbox`,Go 同时核对映射后的 requested 字段和 canonical 生效模式。timeoutMs 显式值必须为 1–2,147,483,647 的安全整数,省略使用上游 600,000 毫秒;worker 不接受通过 0 关闭期限。Go 传递有效 wall deadline 和 high 默认 thinking;worker 将 reasoningEffort 映射到 machine 的 effort,固定 threadMode=ephemeral、command=`FLYWHEEL_CODEX_BIN` 或 `codex`,不恢复旧线程。
103
111
 
104
112
  **这里与 DSH worker 的 EOF 语义不同:Go 写入一行后保持 stdin 开放。** stdin 是父进程的存活连接;未完成前 EOF、读取错误或第二条非空输入触发 AbortSignal,并等待插件有界清理。不能用 `echo … | worker` 的关闭 stdin 行为冒充正常调用。运行请求首行等待上限 10 秒;请求最多 1 MiB,machine 原始输出上限 1 MiB,序列化 progress 累计最多 1 MiB,完整终态帧最多 8 MiB。超限明确失败,不能裁掉回执后判成功。
105
113
 
@@ -119,7 +127,7 @@ Go 的 `DSHArgs.Doctor` 使用这个入口;为保持 v1 必填 `prompt` 的 ad
119
127
 
120
128
  启动前不能获得 machine receipt 时为 null。运行已开始则即使失败也保留完整 receipt;`error` 非 null、重复/丢失 result、尾部垃圾、请求身份或 adapter 版本不符都不能成功。本通道没有 DSH `ready` 帧,不能据此虚构运行前完整沙箱握手或实际 effort 回显。
121
129
 
122
- Go 同时校验外层 worker 物理终态和插件 receipt:请求及 run/thread/turn 身份、模型、cwd、approval=never、实际沙箱模式、完整 sandboxPolicy、stopReason、cleanup 与执行事实。workspaceWrite 的显式 writableRoots 必须处于工作区内,networkAccess 必须明确 false;Codex 标准临时目录标记保留,不宣称只能写 cwd。插件 0.8.0 只在 turn 前检查模式等基本配置;完整策略是返回后的交付资格检查,**不能阻止自定义 Codex 配置已经允许的访问**。
130
+ Go 同时校验外层 worker 物理终态和插件 receipt:请求及 run/thread/turn 身份、模型、cwd、approval=never、实际沙箱模式、完整 sandboxPolicy、stopReason、cleanup 与执行事实。受限模式的 workspaceWrite 显式 writableRoots 必须处于工作区内,networkAccess 必须明确 false;显式完全权限要求 `sandbox:danger-full-access` 与精确的 `{type:"dangerFullAccess"}` policy,不要求不存在的受限模式字段;Codex 标准临时目录标记保留,不宣称只能写 cwd。插件 0.8.0 只在 turn 前检查模式等基本配置;完整策略是返回后的交付资格检查,**不能阻止自定义 Codex 配置已经允许的访问**。
123
131
 
124
132
  effort 的证据只可为 turn/start 请求接受,保留 `effectiveEffort:null`。usage 使用 `run` 增量,不能把 threadTotal 当成本次消耗;只有 complete 才记入可知预算,partial/unknown 原样存入 receipt 并增加未知调用计数。cached input/reasoning output 是总 input/output 的子项,不重复累加。订阅 costUSD 保持 null、costStatus 保持 unknown。receipt 没有实际 provider 时保持未知,不根据 channel 发明 provider。
125
133
 
@@ -175,7 +183,7 @@ config/doctor 的 Go CLI 将 SIGTERM/SIGINT 转为 context 取消,等待 watch
175
183
  | `dispatch` | `not-dispatched` 表示宿主证明本次请求没有启动 Go 控制进程;否则为 `unknown` |
176
184
  | `message` | 给用户诊断的错误文本,本身不证明进程是否启动,也不能作为另一个 envelope 递归解析 |
177
185
 
178
- 只有参数/目录等派发前校验失败、派发前取消,以及进程创建失败且没有成功启动的证据,才能返回 `not-dispatched`。进入控制进程后出现的非零退出、超时、取消、坏 JSON 或任务身份不符都不能取得这项证明,即使错误文本包含相同 JSON。host 对普通错误同样增加外层 envelope,只把原始错误放入 `message`,避免进程返回的文字冒充宿主确认。
186
+ 只有参数/目录等派发前校验失败、派发前取消,以及进程创建失败且没有成功启动的证据,才能返回 `not-dispatched`。省略 cwd 时,host 读取 `workingDirectory.get(agent.session)` 的当前目录;getter 失败、缺会话或缺目录属于派发前失败,不回退 header.cwd。显式 cwd 不读取会话目录。进入控制进程后出现的非零退出、超时、取消、坏 JSON 或任务身份不符都不能取得这项证明,即使错误文本包含相同 JSON。host 对普通错误同样增加外层 envelope,只把原始错误放入 `message`,避免进程返回的文字冒充宿主确认。
179
187
 
180
188
  界面只有在版本、类型、action、dispatch 和 message 都符合契约时才使用宿主证明。未知协议、旧 host 的纯文本错误、DSH 传输失败或丢失响应均保持 `unknown`。启动/停止的 `unknown` 结果必须先成功查询原任务状态,再决定后续变更;不得自动重试。已确认未派发的拒绝会显示原因,用户可以修正输入或环境后重新提交。一次未派发的状态查询也不能解除更早启动/停止留下的未确认结果。
181
189
 
package/README.en.md CHANGED
@@ -7,7 +7,7 @@ provides its DSH SDK and Codex Pro workers. The engine owns task state, budgets,
7
7
  verification, Git delivery, and process lifecycles. The desktop is a control and
8
8
  observation interface; it does not need a parent model session to keep a task alive.
9
9
 
10
- This guide describes the **Flywheel 0.28.1** interfaces. Source availability does
10
+ This guide describes the **Flywheel 0.28.3** interfaces. Source availability does
11
11
  not mean that npm `latest` already contains them. Check the registry version and
12
12
  the [release guide](https://github.com/ckanner/flywheel/blob/main/docs/en/releasing.md) before installing. The desktop
13
13
  compatibility baseline is **DSH Mac 0.2.1-rc.5**, not a claim that every DSH build
@@ -17,6 +17,10 @@ or provider adapter is compatible.
17
17
  uses the composer popup. Upgrade the CLI and desktop plugin to the same new
18
18
  version; merging source does not update an installed plugin.
19
19
 
20
+ Subscription preset migration and `permissions.mode` are available from **0.28.2**. Upgrades preserve existing configuration; only an explicit migration backs it up and updates model bindings and permissions.
21
+
22
+ **Changes in 0.28.3:** account defaults, account-error handling, and the current session directory. Version 0.28.2 still uses API-key defaults and the original directory. Reinstalling that version does not include these fixes. Upgrades preserve configuration; adopting the account preset requires explicit migration.
23
+
20
24
  ## Components
21
25
 
22
26
  | Component | Responsibility |
@@ -87,16 +91,22 @@ workers, not the complete Flywheel application.
87
91
 
88
92
  ## Prepare the SDK profile and selected models
89
93
 
90
- The native DSH worker uses `sdk` by default. A provider installed or authorized
91
- in the desktop profile is not automatically present in the SDK profile.
92
- `FLYWHEEL_DSH_PROFILE` can select another SDK profile, but cannot select
93
- `desktop`. Keep `DSH_HOME` consistent with the configuration you intend to use.
94
-
95
- The native DeepSeek provider is `deepseek-official`, with `deepseek-flash` in
96
- the default configuration. At the checked rc.5 baseline its effort values are
97
- `off`, `low`, `high`, and `max`; there is no `medium`. Other providers have their
98
- own accepted values. For native GPT routes, install and authorize the selected
99
- ChatGPT adapter in the SDK profile. One checked compatibility example is:
94
+ The native worker uses `sdk` by default. Its standard base already includes the
95
+ DeepSeek account service and both account/API-key adapters. With the same
96
+ `DSH_HOME`, standard desktop and SDK profiles share the account credential store;
97
+ the account route needs no additional API key or token copy. Check custom homes,
98
+ credential stores, and plugin sets separately. `FLYWHEEL_DSH_PROFILE` can select
99
+ another SDK profile, but cannot select the reserved `desktop` profile.
100
+
101
+ Current source explicitly selects `deepseek-account` and `deepseek-flash`.
102
+ `deepseek-official` is the separate API-key provider and remains an explicit
103
+ alternative. Desktop default-model selection does not control an independent
104
+ SDK session. The default preset has no cross-provider fallback for authentication or quota failures; explicit user-defined fallback chains keep their existing policy.
105
+ At the checked rc.5 baseline Flash accepts `off`, `low`, `high`, and `max`,
106
+ defaulting to high; it accepts neither medium nor xhigh. Other providers have
107
+ their own values; see [Reasoning effort](https://github.com/ckanner/flywheel/blob/main/docs/en/model-policy.md).
108
+ For native GPT routes, install and authorize the selected ChatGPT adapter in
109
+ the SDK profile. One checked compatibility example is:
100
110
 
101
111
  ```sh
102
112
  dsh plugin --profile sdk add dsh-plugin-chatgpt@0.1.17
@@ -160,12 +170,28 @@ actual inference result. Keep `warn`, `unknown`, and `skipped` distinct from
160
170
 
161
171
  ## Use the desktop controls
162
172
 
163
- Open a DSH session and choose **Flywheel** in the native view bar alongside
164
- Chat and Trajectory. If the entry is not directly visible, check that bar's
165
- More menu. Select the absolute task worktree path as the repository directory
166
- and the task directory inside that same worktree. A blank repository field
167
- uses the current session's starting directory. Prepare and review the contracts
168
- first; the view does not turn an empty directory into a complete task plan.
173
+ The normal entry is `/flywheel-new` in DSH's composer. The assistant prepares
174
+ the description and acceptance files and reports their paths. Review them,
175
+ then use `/flywheel-run`; no panel fields need filling first. Preparation uses
176
+ the current conversation model. After startup, the engine uses Flywheel's
177
+ per-stage model configuration, independently of the chat selection.
178
+
179
+ To inspect or control an existing task, choose **Flywheel** beside Chat and
180
+ Trajectory, or in that view bar's More menu when necessary.
181
+
182
+ | Field | Purpose | When to fill it |
183
+ |---|---|---|
184
+ | Task files directory | Prepared Spec, packets, acceptance description, and task records in `docs/<slug>/`, e.g. `/Users/me/project/docs/fix-login` | Required for panel start/refresh/stop; optional for configuration and diagnostics. Use the path reported by the preparation Skill. |
185
+ | Project directory (optional) | Source project or task integration worktree, e.g. `/Users/me/project` | Current source defaults to the session's effective working directory; override for another project/worktree. |
186
+
187
+ From 0.28.3, Flywheel reads the host's `workingDirectory.get(session)`, following
188
+ directory changes. An explicit project path wins. Getter failures stop before
189
+ dispatch without falling back to the original `header.cwd`. Version 0.28.2
190
+ still uses the original directory, so explicitly fill the project path after
191
+ switching worktrees. The panel does not automatically bind tasks created by a
192
+ Skill: paste its reported task directory and refresh. These are project paths,
193
+ not plugin installation paths. The direct `/flywheel {…}` command is distinct
194
+ from the `/flywheel-*` Skill entries.
169
195
 
170
196
  The panel, `/flywheel`, and `flywheel_control` share these Go operations:
171
197
 
@@ -188,6 +214,13 @@ entry point, select its task directory and refresh. Effective configuration come
188
214
  from the engine's selected file; installing the plugin or changing DSH's chat
189
215
  model does not migrate that configuration.
190
216
 
217
+ **Check installation and configuration** performs local checks without starting
218
+ the SDK. **Check DSH connection** additionally starts a temporary SDK process,
219
+ checks its configuration, directory and permissions, and closes it. Use these
220
+ when installing, upgrading or troubleshooting; they are not required before
221
+ every task. Neither sends an inference prompt or proves account access, quota
222
+ or actual model response capability.
223
+
191
224
  ### View, request, and task lifetimes
192
225
 
193
226
  Task and repository drafts and fetched snapshots are retained separately for
@@ -271,12 +304,15 @@ Conversely, desktop installation and sign-in do not prove that the SDK profile
271
304
  loads and authorizes the same capabilities. Verify each selected harness through
272
305
  its own configuration and supported authorization flow.
273
306
 
274
- The DSH worker requests `read-only` or `workspace-write`, fixes approvals to
275
- `never`, and verifies the effective SDK policy before sending the task prompt.
276
- In the checked Mac source, the DSH sandbox uses its Seatbelt runner and filesystem
277
- fence. Workspace writes include DSH's permitted temporary area; this is not a
278
- network-isolation guarantee. Missing or incomplete enforcement is a failure,
279
- not a reason to switch to unrestricted access.
307
+ The DSH worker supports `read-only`, `workspace-write`, and explicit
308
+ `danger-full-access`, with approvals fixed to `never`. New subscription presets
309
+ select `permissions.mode:full-access` for every stage; Kimi print retains native
310
+ auto behavior. Existing files without this setting retain role-based access
311
+ until explicitly migrated. The actual SDK policy is checked before prompting.
312
+ Restricted modes use the checked Mac Seatbelt runner and filesystem fence;
313
+ workspace writes include DSH's permitted temporary area. Explicit full access
314
+ is recorded without claiming OS confinement. Host static denials, account
315
+ restrictions, and operating-system permissions remain in effect.
280
316
 
281
317
  The SDK overlay disables worker-owned delegation, workflow/goal drivers, model
282
318
  titles, and automatic model compaction so the Go owner can bound work and
@@ -286,14 +322,15 @@ not a claim that every DSH web operation requires a model: the official
286
322
  [web tool](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/web/tool-web/README.md)
287
323
  configures search and fetch independently, while the
288
324
  [DeepSeek search backend](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/web/web-search-deepseek/README.md)
289
- does use an auxiliary model request. This UI fix keeps worker policy unchanged.
325
+ does use an auxiliary model request. Full access changes the host access policy;
326
+ these tool and auxiliary-model restrictions still apply.
290
327
  Restoring retrieval or [automatic compaction](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/compaction/compaction-basic/README.md)
291
328
  requires a separate check of accounting, budgets, cancellation, and directory
292
329
  ownership; the view's location does not establish those guarantees.
293
330
 
294
331
  Codex Pro uses fresh ephemeral calls and checks the model, directory, approval
295
332
  policy, and sandbox mode before execution. In version 0.8.0, the full
296
- `sandboxPolicy` reaches Flywheel only in the returned receipt. Flywheel rejects
333
+ `sandboxPolicy` reaches Flywheel only in the returned receipt. In restricted modes, Flywheel rejects
297
334
  delivery when that policy permits unexpected writable roots or network access,
298
335
  but **this check happens after execution and cannot undo access already
299
336
  permitted by custom Codex configuration**. It is not a complete pre-execution
@@ -316,11 +353,17 @@ escaped process has been cleaned up.
316
353
 
317
354
  Complete receipts preserve configuration, usage, and termination evidence.
318
355
  Accepted effort is not proof of the server's effective reasoning effort.
356
+ DSH's `ready.reasoningEffort` echoes the request after successful initialization.
357
+ Known account sign-in/token errors and the exact account-signed-out hook map to
358
+ AUTH; account quota errors remain QUOTA. Original diagnostics and account
359
+ recovery guidance are preserved. The default preset has no API-key fallback.
319
360
  Unreported usage and dollar cost remain unknown; subscriptions are not recorded
320
361
  as free calls. The current DSH bridge does not support session resume or image
321
362
  and attachment inputs. Its protocol and implementation details are documented
322
363
  in [PROTOCOL.md](PROTOCOL.md) (Chinese).
323
364
 
365
+ From 0.28.3, a started DSH worker without a trustworthy terminal receipt, including a missing result or malformed stream, records unconfirmed SDK cleanup and ends the run without automatic retry or fallback. Reaping the outer worker does not replace an SDK shutdown receipt. Before resuming, confirm that the SDK-managed processes from that call have stopped.
366
+
324
367
  ## Upgrade and maintain the desktop entry
325
368
 
326
369
  The installer records an absolute Node/CLI launcher under
package/README.md CHANGED
@@ -8,6 +8,10 @@
8
8
 
9
9
  **Flywheel 0.28.1 引入原生会话视图**;0.28.0 仍使用输入区底部弹层。需要升级 CLI 与桌面插件到同一新版本,源码合并不会自动更新已安装的插件。
10
10
 
11
+ 订阅预设迁移命令和 `permissions.mode` 从 **0.28.2** 开始提供。升级保留已有配置;只有用户明确应用迁移时才备份并更新模型与权限。
12
+
13
+ **0.28.3 的变化:**新增 `deepseek-account` 默认、账号错误处理与会话当前工作目录修复。0.28.2 仍使用 API 默认和会话最初目录,重装同版不会获得修复;升级保留已有配置,采用账号预设需要显式迁移。
14
+
11
15
  ## 提供什么
12
16
 
13
17
  | 部分 | 行为 |
@@ -60,23 +64,23 @@ Agent 可以加 `--json` 获取单个结构化结果。该命令全局安装本
60
64
 
61
65
  npm CLI 启动器自动解析同版本依赖中的 worker 绝对路径,并在用户未设置时注入 `FLYWHEEL_DSH_WORKER`。直接运行 Go 二进制时默认从 PATH 查找 `flywheel-dsh-worker`;也可显式设置 `FLYWHEEL_DSH_WORKER=/absolute/path/to/dist/worker.js`。自动 launcher 已记录安装时的桌面 `dsh` 入口,用户显式 `FLYWHEEL_DSH_BIN` 优先。`FLYWHEEL_DSH_PROFILE` 默认 `sdk`;可用另一个明确基于 SDK 的独立 profile,但不能是 `desktop`。
62
66
 
63
- 桌面有 adapter 不表示 SDK profile 也加载了它。SDK 的 DeepSeek 官方路由可自动挂载,其余 provider 必须在 **SDK profile** 中预先安装并完成该 provider 的授权配置。例如需原生 ChatGPT 路由时,先按该插件的文档检查并安装到对应 profile:
67
+ 标准 SDK profile 叠加 base,已经包含 DeepSeek 账号服务、账号 adapter 和 API-key adapter。标准桌面与 SDK 在同一 `DSH_HOME` 下默认共用账号授权存储,账号路径不要求复制 token 或额外 API key。自定义 home、凭证存储和 SDK 插件集需核对配置。第三方 provider 则要在 **SDK profile** 中加载并按各自流程授权,例如原生 ChatGPT 路由:
64
68
 
65
69
  ```sh
66
70
  dsh plugin --profile sdk add dsh-plugin-chatgpt@0.1.17
67
71
  ```
68
72
 
69
- 这条命令是配置示例,不会迁移桌面凭证,也不保证一个第三方插件版本自动适配所有宿主。保持用户选择的 `DSH_HOME` 与授权来源一致,按 provider 的正常登录流程配置;不要复制或打印 token。`initialize` 将严格检查请求的 provider、model、effort,未注册的 provider 会失败。
73
+ 这条命令是配置示例,不会复制凭证,也不保证一个第三方插件版本自动适配所有宿主。保持用户选择的 `DSH_HOME` 与授权来源一致,按 provider 的正常登录流程配置;不要复制或打印 token。`initialize` 检查请求的 provider、model、effort,未注册的 provider 会失败。DSH 桌面的账号默认模型选择不自动传给 SDK;Flywheel 必须明确请求所选 provider 与模型。
70
74
 
71
- 实际 Mac rc.5 的 DeepSeek 原生路由是:
75
+ 当前源码默认选择的 Mac rc.5 DeepSeek 账号路由是:
72
76
 
73
77
  ```text
74
- provider = deepseek-official
78
+ provider = deepseek-account
75
79
  model = deepseek-flash
76
80
  reasoningEffort = off | low | high | max
77
81
  ```
78
82
 
79
- `high` 是默认平衡档,**没有 `medium`**。其他 provider 使用自身的合法 effort。SDK README 的历史示例可能仍写 `deepseek-v4-flash`;以所用发行包与 adapter 的实际 catalog 为准。本桥接不替换模型 slug、不跨 provider 降级。
83
+ 本项目核对的 DSH rc.5 adapter 只接受 `off/low/high/max`,默认 high,**不接受 `medium` 或 `xhigh`**;这不同于 DeepSeek 通用 API 的兼容别名。`deepseek-official` 是另一个 API-key provider,只在用户明确选择 API 接入时配置,不复用账号 token。默认预设没有 API 后备,因此未登录、登录失效或额度不足不会自动转用 API;用户显式配置的后备链按原规则执行。其他 provider 使用各自合法 effort,GPT 支持 xhigh/max,Kimi K3.1 Pro 原生支持 low/high/max;理由与评估边界见[推理等级说明](https://github.com/ckanner/flywheel/blob/main/docs/model-policy.md)。SDK README 的历史示例可能仍写 `deepseek-v4-flash`;以所用发行包与 adapter 的实际 catalog 为准。本桥接不替换模型 slug、不跨 provider 降级。
80
84
 
81
85
  ## Codex Pro 插件执行通道
82
86
 
@@ -84,7 +88,7 @@ reasoningEffort = off | low | high | max
84
88
 
85
89
  npm 启动器自动解析并注入 `FLYWHEEL_CODEX_PRO_WORKER`。独立 Go 二进制默认从 PATH 查找 `flywheel-codex-pro-worker`,源码开发可显式设为构建后的 `dist/codex-pro-worker.js`。`FLYWHEEL_CODEX_BIN` 可指定底层 Codex 可执行程序,默认 `codex`;`CODEX_HOME` 沿用用户选择的配置/登录目录。Go 会将这些运行必需项交给插件,不把凭证复制到配置、契约或 Git。
86
90
 
87
- 新调用固定使用 ephemeral 会话和 `read-only`/`workspace-write`,approval 为 never。插件运行前核对模型、cwd 和权限模式;完整 sandboxPolicy 由 Flywheel 在返回后审查,允许 Codex 标准临时目录、拒绝工作区外额外 roots 与网络启用。0.8.0 尚无公开的运行前完整策略 guard,事后拒绝交付不能阻止先前的访问;可信 Codex 配置仍是本机运行前提。
91
+ 新调用使用 ephemeral 会话,权限由 `permissions.mode` 决定,approval 为 never。role-based 选择 `read-only`/`workspace-write`;full-access 请求 `danger-full-access`。插件运行前核对模型、cwd 和权限模式,完整 sandboxPolicy 由 Flywheel 在返回后审查。受限模式允许 Codex 标准临时目录、拒绝工作区外额外 roots 与网络启用;完全权限则核对 `dangerFullAccess` 策略,不声称 OS 隔离。0.8.0 尚无公开的运行前完整策略 guard,事后拒绝交付不能阻止先前的访问;可信 Codex 配置仍是本机运行前提。
88
92
 
89
93
  完整 machine receipt 写入指标,保留 accepted effort、未知 effectiveEffort、usage 完整性与进程终态。插件正常收尾可能主动 TERM/KILL app-server,其物理 exit code 可以为空;Flywheel 只有在完整终态证明任务完成、清理已确认且外层 worker 干净退出时才能继续。取消、超时、截断、启动或清理失败均阻止成功。Go 为此 worker 预留至少 10 秒终止宽限,避免截断插件内部清理。
90
94
 
@@ -111,7 +115,7 @@ flywheel doctor --cwd /repo --task-dir /repo/docs/my-task --probe --json
111
115
  只调试单个 worker 时,仍可在已有且可访问的目标目录生成一条 probe 请求:
112
116
 
113
117
  ```sh
114
- node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1,requestId:"local-doctor",cwd:process.cwd(),provider:"deepseek-official",model:"deepseek-flash",reasoningEffort:"high",permissionMode:"read-only",prompt:"This text is not sent in doctor mode",timeoutMs:20000}))' \
118
+ node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1,requestId:"local-doctor",cwd:process.cwd(),provider:"deepseek-account",model:"deepseek-flash",reasoningEffort:"high",permissionMode:"read-only",prompt:"This text is not sent in doctor mode",timeoutMs:20000}))' \
115
119
  | flywheel-dsh-worker --doctor
116
120
  ```
117
121
 
@@ -121,7 +125,16 @@ node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1
121
125
 
122
126
  ## 桌面使用
123
127
 
124
- 打开一个 DSH 会话,在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**。入口由 DSH 的原生视图栏管理;若未直接显示,检查该栏的更多视图入口。填写已准备并审阅契约的任务目录,例如 `/repo/docs/my-task`。仓库目录默认取当前 DSH 会话的起始目录,也可填明确的绝对路径;生产任务应显式选择任务集成 worktree 及其内部的任务目录。按钮只执行以下 Go JSON 控制接口:
128
+ 正常入口是在 DSH 对话输入框调用 `/flywheel-new`,让助手准备任务说明与验收清单并给出实际路径;审阅后通过 `/flywheel-run` 开始工作。无需先填写面板。初始准备使用当前对话选择的模型,引擎启动后才使用 Flywheel 配置中的各阶段模型;两者不自动同步。
129
+
130
+ 要查看或直接控制已有任务,再在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**;未直接显示时检查该栏的更多视图入口。
131
+
132
+ | 字段 | 用途 | 是否需要填写 |
133
+ |---|---|---|
134
+ | 任务资料目录 | Spec、packet、验收说明和任务记录所在的 `docs/<slug>/`,例如 `/Users/me/project/docs/fix-login` | 面板启动/刷新/停止需指定;配置与诊断可以留空。由准备 Skill 给出,无需自行编写这些文件才能使用入口 |
135
+ | 项目目录(可选) | 对应源码项目/任务集成 worktree,例如 `/Users/me/project` | 当前源码默认读取会话实际工作目录;只在选择另一项目或 worktree 时覆盖 |
136
+
137
+ 从 0.28.3 开始调用宿主 `workingDirectory.get(session)`,跟随会话中的目录切换;显式项目目录优先,getter 失败在派发前报错,不回退到会话最初的 `header.cwd`。0.28.2 仍取最初目录,切换 worktree 后请显式填写项目路径。面板不自动绑定 Skill 创建的任务;查看它时粘贴 Skill 返回的任务目录,再刷新。两个字段都是项目路径,不是插件安装目录。按钮只执行以下 Go JSON 控制接口:
125
138
 
126
139
  ```sh
127
140
  flywheel control status --task-dir /repo/docs/my-task --cwd /repo
@@ -137,9 +150,9 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
137
150
  /flywheel {"action":"status","taskDir":"/repo/docs/my-task","cwd":"/repo"}
138
151
  ```
139
152
 
140
- 模型工具 `flywheel_control` 接受 `action`、`taskDir`、`cwd`,以及仅用于 doctor 的可选 `probe` 布尔值。config/doctor 可省 taskDir,cwd 可沿用会话目录;start/status/stop 必须指定任务。输入统一校验,使用 argv 调用,不拼接 shell 命令。打开视图不会启动任务或探测;视图只在点击时读取状态,不自动轮询,也不自动发现其他入口启动的任务。查看已有任务时,选择同一任务目录再刷新。
153
+ 模型工具 `flywheel_control` 接受 `action`、`taskDir`、`cwd`,以及仅用于 doctor 的可选 `probe` 布尔值。config/doctor 可省 taskDir,cwd 可沿用会话当前工作目录;start/status/stop 必须指定任务。`/flywheel {…}` 是直接人工控制命令,与 `/flywheel-new` 等 Skill 入口不同。输入统一校验,使用 argv 调用,不拼接 shell 命令。打开视图不会启动任务或探测;视图只在点击时读取状态,不自动轮询,也不自动发现其他入口启动的任务。
141
154
 
142
- “查看模型配置”展示配置来源、展开后的阶段绑定及 provider/channel;“检查环境”不启动 SDK,“SDK 无推理检查”显式启用 probe。doctor 的合法退出码 1 会显示逐项诊断,超时、坏协议或不匹配的任务身份仍视为控制错误。`ok:true` 不代表账号、额度和真实推理通过。
155
+ “查看模型配置”展示实际配置来源、展开后的阶段绑定及 provider/channel/权限。“检查安装与配置”仅做本地预检,不启动 SDK;“检查 DSH 接入”额外临时启动 SDK、核对配置/目录/权限后退出。首次安装、升级或排障时按需使用,平时不必每次点击;两项都不向模型发送消息,也不验证账号、额度和实际回答能力。doctor 的合法退出码 1 会显示逐项诊断,超时、坏协议或不匹配的任务身份仍视为控制错误。`ok:true` 不代表账号、额度和真实推理通过。
143
156
 
144
157
  状态区区分未初始化、运行、已完成、暂停、停止、升级和预算停止;全部 packet 可见,不会只展示前几个。未知 usage/cost 明确保留,原始 JSON 默认折叠。配置显示引擎实际选择的文件及阶段绑定;已有用户配置不会因安装或切换 DSH 对话模型而自动迁移。
145
158
 
@@ -165,9 +178,11 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
165
178
 
166
179
  每个调用使用新的 SDK Session;provider/model/effort 在该进程中固定。v1 不支持 resume、图像/附件输入或 SDK 原生 output schema。文本与报告文件由 Go 的冻结合同和 JSON Schema 验收,不能把 SDK idle 当成验收完成。
167
180
 
168
- worker 只支持 `read-only` 与 `workspace-write`。Mac 上由 DSH 的 Seatbelt bash runner 和 native fs fence 执行权限。workspace-write 的写入范围是**实际工作目录加 DSH 规定的系统临时区**,并非额外目录列表,也不是网络隔离。策略插件将 approval 固定为 `never`,即拒绝提权。
181
+ SDK 接受模型与 effort 配置不代表远端服务证明了实际推理强度。`ready.reasoningEffort` 是初始化成功后的请求回显;只有实际任务评估才能判断更高 effort 是否改善交付。账号未登录和 token 失效精确归 AUTH,退出账号导致的已知取消事件同样归 AUTH;账号额度不足归 QUOTA。保留原始原因并提示恢复同一账号;默认预设不提供 API Key 后备。
182
+
183
+ worker 支持 `read-only`、`workspace-write` 和显式 `danger-full-access`。新订阅预设通过 `permissions.mode:full-access` 为全部阶段选择完全权限,approval 固定为 `never`;Kimi print 使用原生 auto,不逐步请求用户点击允许。已有文件缺省仍保留 role-based,需通过[配置迁移](../../docs/configuration.md#将旧配置切换到已有的三种资源)显式采用新方案。受限模式由 Mac 上 DSH 的 Seatbelt bash runner 和 native fs fence 执行,workspace-write 范围是**实际工作目录加 DSH 规定的系统临时区**。full-access 如实记录无 OS 沙箱限制;宿主静态拒绝、账号与操作系统权限不被绕过。
169
184
 
170
- 执行报告先写到 worktree 内的任务报告路径,由 Go 校验后归档到主任务目录。不得为报告写入而把整个任务目录或用户目录设成 full access。
185
+ 执行报告仍先写到 worktree 内的任务报告路径,由 Go 校验后归档到主任务目录。权限模式来自明确配置,不能因为报告写入失败而暗中扩大它;任务边界、冻结验收和候选身份检查保持有效。
171
186
 
172
187
  默认覆盖会关闭 DSH 自主委派、workflow、goal 驱动、模型生成标题及自动模型压缩,避免在一个 Go 工作单元之外再启动辅助模型调用。需要压缩/继续执行时由 Go 显式决定下一次任务;超出上下文或输出 token 限制不会伪装成成功。SDK profile 应只装所需的可信 provider /工具,第三方 Host 插件本身是进程内代码,不能由本包替它提供任意代码隔离。
173
188
 
@@ -177,7 +192,7 @@ worker 只支持 `read-only` 与 `workspace-write`。Mac 上由 DSH 的 Seatbelt
177
192
 
178
193
  ## 生命周期与可验证范围
179
194
 
180
- 官方 rc.5 SDK 没有 per-turn cancel,所以使用每调用一个 SDK 进程。worker 请求 `shutdown` 并等待退出;SDK 自己排空其托管命令。Go 继续负责外层 deadline、取消及它拥有的进程组清扫。正常 SDK 关闭未被证明时结果失败,不能进入成功验收。Go 默认 5 秒 kill grace 应保留,以给 worker 清理窗口;不要配置短于桥接清理预算的值。
195
+ 官方 rc.5 SDK 没有 per-turn cancel,所以使用每调用一个 SDK 进程。worker 请求 `shutdown` 并等待退出;SDK 自己排空其托管命令。Go 继续负责外层 deadline、取消及它拥有的进程组清扫。正常 SDK 关闭未被证明时结果失败,不能进入成功验收。worker 已启动但终态回执缺失或协议损坏时会记录清理未确认并停止本次运行,不自动重试或降级;恢复前先确认该次调用的 SDK 托管进程已停止。Go 默认 5 秒 kill grace 应保留,以给 worker 清理窗口;不要配置短于桥接清理预算的值。
181
196
 
182
197
  测试覆盖离线 worker 全生命周期与事件折叠;桌面 command/tool 入口一致;构建后的浏览器模块在 React 与 jsdom 中运行插件自有控件、store handle 和 observable。组件夹具适配槽位桥接和 Remote,另用已发布的 rc.5 renderer、slots 与 Cordis 检验真实注册、会话绑定、声明收起/恢复及卸载;这两类测试都不验证真实宿主几何布局。还覆盖 Go 与 Node 的配置、诊断和失败契约,以及安装记录校验、用户配置优先和损坏入口拒绝。根目录的 npm 安装测试使用实际六包 tarball、隔离目录和 DSH 协议桩,验证完整安装流程。前轮还核对官方 npm rc.5 的真实 Host commands/tools 注册、SDK 初始化/关闭,以及当前 Linux 无可用沙箱时在 prompt 前 fail closed。前轮运行结果见[验证记录](../../docs/validation/2026-11-01-results.md)。
183
198
 
package/dist/bridge.js CHANGED
@@ -75,7 +75,10 @@ export class Runtime {
75
75
  });
76
76
  this.child.on('close', (code, signal) => {
77
77
  this.closed = true;
78
- if (this.buffer.length) this.fail(new BridgeError('TRANSPORT', 'DSH stdout ended in an incomplete frame'));
78
+ if (this.buffer.length) {
79
+ this.protocolFailure ??= new BridgeError('TRANSPORT', 'DSH stdout ended in an incomplete frame');
80
+ this.fail(this.protocolFailure);
81
+ }
79
82
  this.fail(new BridgeError('TRANSPORT', `DSH transport closed (exit=${code}, signal=${signal ?? 'none'})`));
80
83
  this.resolveExit(this.exit ?? { code, signal });
81
84
  });
@@ -265,7 +268,14 @@ export async function runWorker(input, { emit, env = process.env, signal, doctor
265
268
  settled = false;
266
269
  failure ??= { code: 'TRANSPORT', message: 'DSH runtime did not prove graceful shutdown of its managed processes' };
267
270
  }
268
- if (runtime.protocolFailure) failure ??= normalizedError(runtime.protocolFailure);
271
+ if (runtime.protocolFailure) {
272
+ // A provider result cannot attest a damaged SDK stream. Keep its
273
+ // diagnosis as context, while protocol and cleanup facts stay independent.
274
+ const protocolFailure = normalizedError(runtime.protocolFailure);
275
+ failure = failure && failure.message !== protocolFailure.message
276
+ ? { ...protocolFailure, message: `${protocolFailure.message}; preceding failure: ${failure.message}` }
277
+ : protocolFailure;
278
+ }
269
279
  } catch (error) {
270
280
  settled = false;
271
281
  failure = normalizedError(error);
package/dist/client.js CHANGED
@@ -96,26 +96,28 @@ function createViewStore() {
96
96
 
97
97
  const zh = {
98
98
  view: 'Flywheel',
99
- title: '任务控制',
100
- description: '查看 Flywheel 任务的进度与验收结果,在需要时启动、恢复或停止任务',
99
+ title: 'Flywheel 任务',
100
+ description: '准备任务后,在这里查看进度、启动、继续或停止',
101
101
  lifecycle: '任务在后台独立运行。切换视图或会话不会停止任务;需要停止时请使用这里的“停止”',
102
- selection: '选择任务',
103
- taskDir: '任务目录',
104
- taskHint: '填写已准备好契约的绝对路径;只查看配置或检查环境时可留空',
105
- cwd: '仓库目录',
106
- cwdHint: '留空使用当前 DSH 会话目录,也可指定任务 worktree 的绝对路径',
107
- pathInvalid: '请输入以 / 开头的绝对路径',
102
+ selection: '打开已有任务',
103
+ taskDir: '任务资料目录',
104
+ taskHint: '填写助手准备任务时给出的完整路径,里面保存任务说明和验收清单;只查看配置或做检查时可留空',
105
+ cwd: '项目目录(可选)',
106
+ cwdHint: '留空时使用当前 DSH 会话的工作目录。若它不是本任务的项目根目录,填写助手给出的项目完整路径',
107
+ pathInvalid: '请填写以 / 开头的完整路径',
108
108
  pathControl: '路径不能包含换行或空字符',
109
- taskRequired: '填写任务目录后可刷新、启动或停止任务',
109
+ taskRequired: '填写任务资料目录后,可刷新状态、启动或停止任务',
110
110
  status: '刷新状态',
111
111
  start: '启动',
112
112
  stop: '停止',
113
113
  config: '查看模型配置',
114
- doctor: '检查环境',
115
- probe: 'SDK 无推理检查',
114
+ doctor: '检查安装与配置',
115
+ probe: '检查 DSH 接入',
116
+ doctorHelp: '检查安装与配置:核对本机工具、模型配置和所选任务文件,不会启动任务',
117
+ probeHelp: '检查 DSH 接入:临时启动独立的 DSH 运行环境,检查配置、项目目录和权限后退出;可能联网读取模型目录',
116
118
  busy: '正在等待“{action}”确认…',
117
119
  busyOther: '正在等待上一个目录的“{action}”结束;其结果不会覆盖当前选择',
118
- mutationLock: '启动或停止确认前,暂时不能切换任务目录',
120
+ mutationLock: '启动或停止确认前,暂时不能切换任务资料目录或项目目录',
119
121
  unknownOutcome: '“{action}”结果未确认',
120
122
  unknownHelp: '请求可能已经送达引擎。先刷新这个任务的状态,再决定下一步操作',
121
123
  reconcile: '刷新待确认任务',
@@ -127,7 +129,7 @@ const zh = {
127
129
  feedback: '操作反馈',
128
130
  statusHeading: '任务状态',
129
131
  emptyHeading: '还没有任务状态',
130
- emptyBody: '在“对话”中准备并审阅任务契约,然后填写任务目录并刷新状态。已有的后台任务也可在这里继续查看',
132
+ emptyBody: '在“对话”输入 /flywheel-new 和需求即可准备任务,无需先填写本页。审阅后可用 /flywheel-run 和 /flywheel-status 加任务路径操作;如需在本页查看,再填写助手给出的任务资料目录。已有任务使用原目录',
131
133
  emptySelected: '点击“刷新状态”查看这个任务;打开本页不会自动启动任务',
132
134
  lastChecked: '上次确认:{time}',
133
135
  stale: '本次状态未确认,下方保留上次结果',
@@ -171,12 +173,13 @@ const zh = {
171
173
  tokensMissing: ';{count} 次调用用量未知,合计不完整',
172
174
  tokenLimit: '每日 Token 限额:{limit} · 预算策略:{policy}',
173
175
  budgetWarning: '预算提示:{message}',
174
- advanced: '模型配置与环境检查',
175
- advancedHelp: '查看当前任务实际使用的绑定,或在启动前检查本地环境',
176
+ advanced: '配置与排查(按需)',
177
+ advancedHelp: '首次安装、升级或运行失败时可用,平时不必每次点击;两项检查都不向模型发送消息,也不能验证账号、额度或实际回答能力',
176
178
  configuration: '实际模型配置',
179
+ configurationHelp: '这里显示 Flywheel 从下方来源读取的实际生效配置;DSH 对话输入框当前选择的模型不会替代它',
177
180
  source: '配置来源:{source}',
178
181
  embedded: '内嵌模板',
179
- scope: '检查范围:{scope}',
182
+ scope: '配置层级:{scope}',
180
183
  outOfScope: '未用于本次检查',
181
184
  model: '模型',
182
185
  profile: 'Profile',
@@ -185,6 +188,9 @@ const zh = {
185
188
  channel: '渠道',
186
189
  thinking: '推理',
187
190
  permission: '阶段权限要求',
191
+ permissionFull: '完全权限(无需逐步确认)',
192
+ permissionWorkspace: '项目内写入',
193
+ permissionReadOnly: '只读',
188
194
  session: '会话恢复',
189
195
  enabled: '启用',
190
196
  disabled: '关闭',
@@ -202,9 +208,9 @@ const zh = {
202
208
  diagnostic: '环境检查结果',
203
209
  doctorPassed: '检查结束:未发现阻断项',
204
210
  doctorFailed: '检查结束:存在阻断项',
205
- localOnly: '本次只检查本地环境,未启动 SDK 探测',
206
- probeRequested: '已请求 SDK 无推理检查',
207
- inferenceBoundary: '未发送推理请求;账号授权、额度和真实模型能力未通过本次检查验证',
211
+ localOnly: '本次检查本机工具、配置和所选任务文件,未启动 DSH 接入检查',
212
+ probeRequested: '已请求 DSH 接入检查(不调用模型)',
213
+ inferenceBoundary: '没有向模型发送消息;账号授权、可用额度和实际回答能力仍未验证',
208
214
  pass: '通过',
209
215
  fail: '失败',
210
216
  warn: '注意',
@@ -216,26 +222,28 @@ const zh = {
216
222
  };
217
223
  const en = {
218
224
  view: 'Flywheel',
219
- title: 'Task controls',
220
- description: 'Inspect Flywheel progress and verification results, then start, resume or stop a task when needed',
225
+ title: 'Flywheel tasks',
226
+ description: 'After preparing a task, inspect its progress, start, resume or stop it here',
221
227
  lifecycle: 'Tasks run independently in the background. Switching views or sessions does not stop a task; use Stop here when needed.',
222
- selection: 'Select a task',
223
- taskDir: 'Task directory',
224
- taskHint: 'Absolute path to a prepared task contract; optional for configuration and environment checks',
225
- cwd: 'Repository directory',
226
- cwdHint: 'Leave blank to use this DSH session directory, or enter the absolute task worktree path',
227
- pathInvalid: 'Enter an absolute path beginning with /',
228
+ selection: 'Open an existing task',
229
+ taskDir: 'Task files directory',
230
+ taskHint: 'Use the full path supplied by the assistant when it prepared the task instructions and acceptance checks; leave blank to inspect configuration or run diagnostics',
231
+ cwd: 'Project directory (optional)',
232
+ cwdHint: 'Leave blank to use this DSH session’s current working directory. If that is not this task’s project root, enter the full project path supplied by the assistant.',
233
+ pathInvalid: 'Enter a full path beginning with /',
228
234
  pathControl: 'Paths cannot contain line breaks or null characters',
229
- taskRequired: 'Enter a task directory to refresh, start or stop a task',
235
+ taskRequired: 'Enter the task files directory to refresh status, start or stop the task',
230
236
  status: 'Refresh status',
231
237
  start: 'Start',
232
238
  stop: 'Stop',
233
239
  config: 'Inspect model configuration',
234
- doctor: 'Check environment',
235
- probe: 'SDK check without inference',
240
+ doctor: 'Check installation and configuration',
241
+ probe: 'Check DSH integration',
242
+ doctorHelp: 'Installation and configuration: check local tools, model settings and selected task files without starting the task.',
243
+ probeHelp: 'DSH integration: briefly start a separate DSH runtime, verify its configuration, project directory and permissions, then close it. It may fetch a model catalog online.',
236
244
  busy: 'Waiting for “{action}” confirmation…',
237
245
  busyOther: 'Waiting for “{action}” for the previous directory; its result will not replace the current selection',
238
- mutationLock: 'Task directories are locked until start or stop is confirmed',
246
+ mutationLock: 'Task and project directories are locked until start or stop is confirmed',
239
247
  unknownOutcome: '“{action}” outcome is unconfirmed',
240
248
  unknownHelp: 'The request may have reached the engine. Refresh this task before choosing the next action.',
241
249
  reconcile: 'Refresh unconfirmed task',
@@ -247,7 +255,7 @@ const en = {
247
255
  feedback: 'Operation feedback',
248
256
  statusHeading: 'Task status',
249
257
  emptyHeading: 'No task status yet',
250
- emptyBody: 'Prepare and review a task contract in Chat, then enter its directory and refresh. Existing background tasks can also be inspected here.',
258
+ emptyBody: 'In Chat, enter /flywheel-new and your requirement without filling in this view. After review, use /flywheel-run and /flywheel-status with the task path. To inspect it here, enter the task files directory the assistant provides. For an existing task, use its original directory.',
251
259
  emptySelected: 'Refresh status to inspect this task. Opening this view does not start it.',
252
260
  lastChecked: 'Last confirmed: {time}',
253
261
  stale: 'Current status is unconfirmed; the previous result is retained below',
@@ -291,12 +299,13 @@ const en = {
291
299
  tokensMissing: '; usage is unknown for {count} calls, so this total is incomplete',
292
300
  tokenLimit: 'Daily token limit: {limit} · Budget policy: {policy}',
293
301
  budgetWarning: 'Budget notice: {message}',
294
- advanced: 'Model configuration and environment',
295
- advancedHelp: 'Inspect the bindings used by this task or check the local environment before starting',
302
+ advanced: 'Configuration and troubleshooting (optional)',
303
+ advancedHelp: 'Useful after installation, upgrades or a failed run; neither check is required before every task. Neither sends messages to a model or verifies account access, quota or real model responses.',
296
304
  configuration: 'Effective model configuration',
305
+ configurationHelp: 'These are the effective Flywheel settings loaded from the source below. The model selected in the DSH chat composer does not replace them.',
297
306
  source: 'Configuration source: {source}',
298
307
  embedded: 'Embedded template',
299
- scope: 'Check scope: {scope}',
308
+ scope: 'Configuration level: {scope}',
300
309
  outOfScope: 'Not used for this check',
301
310
  model: 'Model',
302
311
  profile: 'Profile',
@@ -305,6 +314,9 @@ const en = {
305
314
  channel: 'Channel',
306
315
  thinking: 'Reasoning',
307
316
  permission: 'Required permission',
317
+ permissionFull: 'Full access (no step-by-step approval)',
318
+ permissionWorkspace: 'Write within the project',
319
+ permissionReadOnly: 'Read only',
308
320
  session: 'Session resume',
309
321
  enabled: 'Enabled',
310
322
  disabled: 'Disabled',
@@ -322,9 +334,9 @@ const en = {
322
334
  diagnostic: 'Environment check results',
323
335
  doctorPassed: 'Check complete: no blockers found',
324
336
  doctorFailed: 'Check complete: blockers found',
325
- localOnly: 'Only the local environment was checked; no SDK probe ran',
326
- probeRequested: 'An SDK check without inference was requested',
327
- inferenceBoundary: 'No inference request was sent. Account authorization, quota and actual model capability remain unverified.',
337
+ localOnly: 'Local tools, configuration and selected task files were checked; no DSH integration check ran.',
338
+ probeRequested: 'A DSH integration check was requested (no model call)',
339
+ inferenceBoundary: 'No message was sent to a model. Account access, available quota and real model responses remain unverified.',
328
340
  pass: 'Passed',
329
341
  fail: 'Failed',
330
342
  warn: 'Notice',
@@ -348,6 +360,11 @@ const packetLabels = {
348
360
  const roleLabels = { plan: 'planRole', execute: 'executeRole', review: 'reviewRole', judge: 'judgeRole',
349
361
  intake: 'intakeRole', garden: 'gardenRole', 'judge-panel': 'panelRole' };
350
362
  const checkLabels = { pass: 'pass', fail: 'fail', warn: 'warn', unknown: 'unverified', skipped: 'skipped' };
363
+ const permissionLabels = {
364
+ 'danger-full-access': 'permissionFull',
365
+ 'workspace-write': 'permissionWorkspace',
366
+ 'read-only': 'permissionReadOnly',
367
+ };
351
368
  const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
352
369
  const textValue = (value, fallback = '—') => typeof value === 'string' || typeof value === 'number' ? String(value) : fallback;
353
370
  const selectionOf = value => ({ taskDir: value.taskDir.trim(), cwd: value.cwd.trim() });
@@ -506,6 +523,7 @@ function Configuration({ value, t }) {
506
523
  h('dt', null, t(name)), h('dd', null, code ? h('code', null, value) : value));
507
524
  return h('section', { className: css.section, 'aria-label': t('configuration') },
508
525
  h('h3', { className: css.heading }, t('configuration')),
526
+ h('p', { className: css.meta }, t('configurationHelp')),
509
527
  h('p', { className: css.meta }, t('source', { source: textValue(value.source, '') || t('embedded') })),
510
528
  h('p', { className: css.meta }, t('scope', { scope: textValue(value.scope) })),
511
529
  h('div', { className: css.bindings }, ...(value.bindings ?? []).map((binding, index) =>
@@ -521,7 +539,8 @@ function Configuration({ value, t }) {
521
539
  field('provider', textValue(binding?.provider, '') || (binding?.harness === 'dsh' ? t('notConfigured') : t('nativeProvider'))),
522
540
  field('channel', textValue(binding?.channel, '') || t('notConfigured')),
523
541
  field('thinking', textValue(binding?.thinking, '') || t('default')),
524
- field('permission', textValue(binding?.permissionMode, '') || t('undeclared')),
542
+ field('permission', Object.hasOwn(permissionLabels, binding?.permissionMode) ?
543
+ t(permissionLabels[binding.permissionMode]) : textValue(binding?.permissionMode, '') || t('undeclared')),
525
544
  field('session', t(binding?.residentSession ? 'enabled' : 'disabled')))))));
526
545
  }
527
546
 
@@ -639,6 +658,7 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
639
658
  const invalidCwd = pathProblem(selection.cwd);
640
659
  const button = (action, labelKey, needsTask, probe = false, variant = 'outline') => h(Button, {
641
660
  key: labelKey, variant, size: 'sm',
661
+ 'aria-describedby': ['doctor', 'probe'].includes(labelKey) ? formId + '-' + labelKey + '-help' : undefined,
642
662
  disabled: busy || !!invalidTask || !!invalidCwd || (needsTask && !selection.taskDir.trim()) ||
643
663
  (mutates(action) && control.uncertainty !== null),
644
664
  onClick: () => run(action, selection, probe),
@@ -650,7 +670,7 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
650
670
  'aria-invalid': !!problem, 'aria-describedby': formId + '-' + name + '-help',
651
671
  onChange: event => changeSelection({ taskDir: selection.taskDir, cwd: selection.cwd, [name]: event.target.value }) }),
652
672
  h('p', { id: formId + '-' + name + '-help', className: problem ? css.errorText : css.meta }, t(problem ?? hint)));
653
- return h('section', { className: css.page, 'aria-label': 'Flywheel ' + t('title') },
673
+ return h('section', { className: css.page, 'aria-label': t('title') },
654
674
  h('style', { 'data-plugin': '@ckanner/dsh-plugin-flywheel',
655
675
  'data-plugin-css': '@ckanner/dsh-plugin-flywheel/client.module.css',
656
676
  }, stylesheet),
@@ -659,8 +679,8 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
659
679
  h('p', { className: css.meta }, t('description'))),
660
680
  h('section', { className: css.selection, 'aria-label': t('selection') },
661
681
  h('div', { className: css.fields },
662
- field('taskDir', 'taskHint', '/path/to/repo/docs/task', invalidTask),
663
- field('cwd', 'cwdHint', '/path/to/repo', invalidCwd)),
682
+ field('taskDir', 'taskHint', '/Users/me/project/docs/fix-login', invalidTask),
683
+ field('cwd', 'cwdHint', '/Users/me/project', invalidCwd)),
664
684
  h('div', { className: css.actions },
665
685
  button('status', 'status', true, false, 'primary'),
666
686
  button('start', 'start', true), button('stop', 'stop', true))),
@@ -674,6 +694,8 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
674
694
  h('div', { className: css.actions },
675
695
  button('config', 'config', false), button('doctor', 'doctor', false),
676
696
  button('doctor', 'probe', false, true)),
697
+ h('p', { className: css.meta, id: formId + '-doctor-help' }, t('doctorHelp')),
698
+ h('p', { className: css.meta, id: formId + '-probe-help' }, t('probeHelp')),
677
699
  h(Diagnostic, { value: control.diagnostic, t }),
678
700
  h(Configuration, { value: control.configuration, t }))),
679
701
  control.raw && h('details', { className: css.details },
@@ -38,7 +38,7 @@ export function machineRequest(value, api, env) {
38
38
  const keys = new Set(['type', 'protocol', 'requestId', 'cwd', 'model', 'reasoningEffort', 'permissionMode', 'prompt', 'timeoutMs']);
39
39
  if (value === null || typeof value !== 'object' || Array.isArray(value) || value.type !== 'start' || value.protocol !== 1 ||
40
40
  Object.keys(value).some(key => !keys.has(key)) || typeof value.requestId !== 'string' || value.requestId.length > 256 ||
41
- !['read-only', 'workspace-write'].includes(value.permissionMode)) {
41
+ !['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
42
42
  throw new WorkerError('INVALID_REQUEST', 'Expected one protocol=1 start request with an explicit supported permission mode');
43
43
  }
44
44
  if (value.timeoutMs !== undefined && (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 2147483647)) {
@@ -46,7 +46,9 @@ export function machineRequest(value, api, env) {
46
46
  }
47
47
  try {
48
48
  return api.parseMachineRequest({ protocol: 1, requestId: value.requestId, cwd: value.cwd, model: value.model,
49
- effort: value.reasoningEffort, permissionMode: value.permissionMode, prompt: value.prompt,
49
+ effort: value.reasoningEffort,
50
+ permissionMode: value.permissionMode === 'danger-full-access' ? 'dangerously-bypass-approvals-and-sandbox' : value.permissionMode,
51
+ prompt: value.prompt,
50
52
  ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
51
53
  command: env.FLYWHEEL_CODEX_BIN || 'codex', threadMode: 'ephemeral', maxOutputBytes: LIMITS.machineOutputBytes });
52
54
  } catch (error) {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ControlNotDispatchedError, runControl } from './control.js';
2
2
  import { resolveFlywheelBinary } from './installation.js';
3
3
 
4
- export const inject = ['commands', 'tools'];
4
+ export const inject = ['commands', 'tools', 'workingDirectory'];
5
5
 
6
6
  export function apply(ctx, config = {}) {
7
7
  // Loading the desktop package accidentally into the SDK profile must not
@@ -16,9 +16,20 @@ export function apply(ctx, config = {}) {
16
16
  if (signal?.aborted) cancel();
17
17
  active.add(controller);
18
18
  try {
19
+ let defaultCwd;
20
+ try {
21
+ // The immutable session header records its original project. Read the
22
+ // public current-directory projection without ensure() recovery effects.
23
+ if (input?.cwd == null && agent?.session) {
24
+ defaultCwd = ctx.workingDirectory.get(agent.session);
25
+ }
26
+ } catch (error) {
27
+ // No control process has been launched if resolving the default fails.
28
+ throw new ControlNotDispatchedError(error);
29
+ }
19
30
  return await runControl(input, {
20
31
  binary,
21
- defaultCwd: agent?.session?.header?.cwd,
32
+ defaultCwd,
22
33
  timeoutMs: config.controlTimeoutMs ?? 60000,
23
34
  signal: controller.signal,
24
35
  });
@@ -70,7 +81,7 @@ export function apply(ctx, config = {}) {
70
81
  properties: {
71
82
  action: { type: 'string', enum: ['start', 'status', 'stop', 'config', 'doctor'] },
72
83
  taskDir: { type: 'string', description: 'Absolute existing task ledger directory; required for start/status/stop' },
73
- cwd: { type: 'string', description: 'Absolute target repository directory; defaults to this DSH session' },
84
+ cwd: { type: 'string', description: 'Absolute target repository directory; defaults to the current working directory of this DSH session' },
74
85
  probe: { type: 'boolean', description: 'Doctor only: explicitly enable SDK checks without inference; defaults to false' },
75
86
  },
76
87
  required: ['action'],
package/dist/policy.js CHANGED
@@ -4,20 +4,22 @@ export const inject = ['sandboxPolicy', 'approval', 'shell'];
4
4
 
5
5
  export async function apply(ctx, config) {
6
6
  const { permissionMode, cwd } = config;
7
- if (!['read-only', 'workspace-write'].includes(permissionMode) || typeof cwd !== 'string' ||
7
+ const fullAccess = permissionMode === 'danger-full-access';
8
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(permissionMode) || typeof cwd !== 'string' ||
8
9
  ctx.shell.sandboxMode === undefined) {
9
- throw new Error('Flywheel requires a confined DSH shell and explicit workspace policy');
10
+ throw new Error('Flywheel requires a sandbox-capable DSH shell and explicit permission policy');
10
11
  }
11
- // This local no-op proves that the actual runner starts and reports full
12
- // enforcement. Merely selecting a policy value is not readiness evidence.
12
+ // Probe the official runner. Restricted modes must prove full enforcement;
13
+ // full access must truthfully report that no confinement was applied.
13
14
  const probe = await ctx.shell.execute(ctx.shell.resolve({
14
15
  command: ':', workdir: cwd, timeoutMs: 5000,
15
16
  sandboxPolicy: { mode: permissionMode, workspaceRoot: cwd },
16
17
  }));
17
18
  const result = await probe.result();
18
19
  if (result.exitCode !== 0 || result.timedOut || result.aborted ||
19
- result.sandbox?.mode !== permissionMode || result.sandbox?.enforcement !== 'full') {
20
- throw new Error('Flywheel sandbox readiness probe did not prove full enforcement');
20
+ result.sandbox?.mode !== permissionMode ||
21
+ (fullAccess ? result.sandbox.denied !== false || result.sandbox.enforcement !== undefined : result.sandbox.enforcement !== 'full')) {
22
+ throw new Error(fullAccess ? 'Flywheel readiness probe did not confirm explicit full access' : 'Flywheel sandbox readiness probe did not prove full enforcement');
21
23
  }
22
24
  ctx.on('session/created', session => {
23
25
  // A fresh SDK session has no inherited permission facts. Pin both canonical
@@ -30,8 +32,8 @@ export async function apply(ctx, config) {
30
32
  cwd: policy.workspaceRoot,
31
33
  permissionMode: policy.mode,
32
34
  approvalPolicy: ctx.approval.effectivePolicy(session),
33
- confinedShell: ctx.shell.sandboxMode !== undefined,
34
- enforcement: result.sandbox.enforcement,
35
+ confinedShell: !fullAccess,
36
+ ...(fullAccess ? {} : { enforcement: result.sandbox.enforcement }),
35
37
  });
36
38
  });
37
39
  }
package/dist/protocol.js CHANGED
@@ -49,8 +49,8 @@ export async function validateStart(value) {
49
49
  const provider = nonempty(value.provider, 'provider');
50
50
  const model = nonempty(value.model, 'model');
51
51
  const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
52
- if (!['read-only', 'workspace-write'].includes(value.permissionMode)) {
53
- throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only or workspace-write');
52
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
53
+ throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only, workspace-write or danger-full-access');
54
54
  }
55
55
  if (value.reasoningEffort !== undefined) nonempty(value.reasoningEffort, 'reasoningEffort', 64);
56
56
  if (value.maxTokens !== undefined && (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1)) {
@@ -72,12 +72,25 @@ export function normalizedError(error) {
72
72
 
73
73
  export function modelErrorCode(code) {
74
74
  if (ERROR_CODES.has(code)) return code;
75
- if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL') return 'AUTH';
75
+ if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL' ||
76
+ code === 'ACCOUNT_SIGN_IN_REQUIRED' || code === 'ACCOUNT_TOKEN_INVALID') return 'AUTH';
76
77
  if (code === 'ACCOUNT_QUOTA') return 'QUOTA';
77
78
  if (code === 'NO_ADAPTER' || code === 'UNSUPPORTED_REASONING_EFFORT') return 'INVALID_REQUEST';
78
79
  return 'SERVER';
79
80
  }
80
81
 
82
+ const ACCOUNT_RECOVERY = new Map([
83
+ ['ACCOUNT_SIGN_IN_REQUIRED', 'Sign in to DeepSeek in DSH, then ensure the SDK runtime can access that account and the configured endpoint permits account authentication before retrying.'],
84
+ ['ACCOUNT_TOKEN_INVALID', 'The DeepSeek account login was rejected. Sign in again in DSH and ensure the SDK runtime can access the renewed login before retrying.'],
85
+ ['ACCOUNT_QUOTA', 'Check the DeepSeek account balance and usage; retry when account quota is available.'],
86
+ ]);
87
+
88
+ function modelErrorMessage(detail, fallback) {
89
+ const message = detail?.message ?? fallback;
90
+ const recovery = ACCOUNT_RECOVERY.get(detail?.code);
91
+ return recovery === undefined ? message : `${detail.code}: ${message}\n${recovery}`;
92
+ }
93
+
81
94
  /** Fold committed per-call records; streamed usage snapshots are never summed. */
82
95
  export class SessionFold {
83
96
  constructor(sessionId) {
@@ -151,10 +164,12 @@ export class SessionFold {
151
164
 
152
165
  verifiedPolicy(request) {
153
166
  const policy = this.policy;
167
+ const fullAccess = request.permissionMode === 'danger-full-access';
154
168
  if (!policy || policy.protocol !== PROTOCOL || policy.permissionMode !== request.permissionMode ||
155
169
  policy.cwd !== request.cwd || policy.approvalPolicy !== 'never' ||
156
- policy.confinedShell !== true || policy.enforcement !== 'full') {
157
- throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and confined permission policy; prompt withheld');
170
+ policy.confinedShell !== !fullAccess ||
171
+ (fullAccess ? policy.enforcement !== undefined : policy.enforcement !== 'full')) {
172
+ throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and permission policy; prompt withheld');
158
173
  }
159
174
  }
160
175
 
@@ -162,9 +177,16 @@ export class SessionFold {
162
177
  if (!this.receipts.has(messageId)) throw new BridgeError('TRANSPORT', 'missing durable prompt receipt');
163
178
  if (!this.terminal) throw new BridgeError('TRANSPORT', 'idle without committed turn/end');
164
179
  if (this.terminal.kind !== 'completed') {
180
+ // DSH's account-tasks hook cancels the active account turn after sign-out,
181
+ // including when rejecting an expired token. Do not infer this from text
182
+ // or treat other cancellation hooks as authentication failures.
183
+ if (this.terminal.kind === 'aborted' && this.terminal.reason?.kind === 'hook' &&
184
+ this.terminal.reason.reason === 'deepseek-account/signed-out') {
185
+ throw new BridgeError('AUTH', 'DeepSeek account signed out (deepseek-account/signed-out). Sign in again in DSH and ensure the SDK runtime can access that account before retrying.');
186
+ }
165
187
  const detail = this.terminal.error;
166
188
  throw new BridgeError(modelErrorCode(detail?.code),
167
- detail?.message ?? `DSH turn ended as ${this.terminal.kind}`);
189
+ modelErrorMessage(detail, `DSH turn ended as ${this.terminal.kind}`));
168
190
  }
169
191
  if (!this.assistantCount || this.text.trim() === '') {
170
192
  throw new BridgeError('EMPTY_RESPONSE', 'DSH completed without assistant text');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckanner/dsh-plugin-flywheel",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
4
4
  "description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",