dsh-cindy-host 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +462 -0
  2. package/LICENSE +21 -0
  3. package/README.md +178 -0
  4. package/cordis.patch.yml +6 -0
  5. package/doc/architecture.md +34 -0
  6. package/doc/cindy-android-verify.md +362 -0
  7. package/doc/cindy-mobile-freeze-handoff.md +124 -0
  8. package/doc/cindy-phone-link.md +2902 -0
  9. package/doc/desktop-runtime-fix-20260930.md +69 -0
  10. package/doc/development-plan.md +19 -0
  11. package/doc/file-peer-apply-recovery-20260921.md +35 -0
  12. package/doc/file-peer-installed-hotfix-20260921.md +53 -0
  13. package/doc/file-peer-minimal-apply-20260921.md +78 -0
  14. package/doc/file-peer-original-contract-alignment-20260921.md +119 -0
  15. package/doc/file-peer-phone-export-observation-20260921.md +75 -0
  16. package/doc/file-peer-product-contract-audit-20260921.md +106 -0
  17. package/doc/file-peer-root-cause-20260921.md +53 -0
  18. package/doc/file-transfer.md +50 -0
  19. package/doc/host-performance-20260930.md +91 -0
  20. package/doc/neonabyss-apk-file-peer-diagnosis-20260921.md +87 -0
  21. package/doc/new-session-sync-fix-20260930.md +59 -0
  22. package/doc/patches/cindy-peer-silence-probe.patch +1112 -0
  23. package/doc/projection-read-probe.md +77 -0
  24. package/doc/publishing.md +259 -0
  25. package/doc/releasing.md +149 -0
  26. package/doc/research.md +24 -0
  27. package/doc/session-discovery-2026-09-20.md +67 -0
  28. package/lib/client.js +763 -0
  29. package/package.json +84 -0
  30. package/src/auth-session.js +189 -0
  31. package/src/authorization-policy.js +59 -0
  32. package/src/cindy-channels.js +1707 -0
  33. package/src/cindy-login-flow.js +159 -0
  34. package/src/cindy-login.js +80 -0
  35. package/src/cindy-message-row.js +432 -0
  36. package/src/cindy-session-row.js +171 -0
  37. package/src/console-sink.js +6 -0
  38. package/src/contracts.js +26 -0
  39. package/src/credential-store.js +128 -0
  40. package/src/demo.js +9 -0
  41. package/src/dsh-active-sessions.js +59 -0
  42. package/src/dsh-host-source.js +62 -0
  43. package/src/dsh-message-fold.js +608 -0
  44. package/src/dsh-plugin.js +1932 -0
  45. package/src/dsh-session-source.js +529 -0
  46. package/src/file-peer-protocol.js +7 -0
  47. package/src/file-peer-rtc.js +41 -0
  48. package/src/fixture-source.js +13 -0
  49. package/src/host-approvals.js +329 -0
  50. package/src/host-attachments.js +249 -0
  51. package/src/host-authorization.js +51 -0
  52. package/src/host-demo.js +3 -0
  53. package/src/host-export-jobs.js +356 -0
  54. package/src/host-export-owner.js +62 -0
  55. package/src/host-file-browser.js +400 -0
  56. package/src/host-file-ice.js +63 -0
  57. package/src/host-file-peer.js +289 -0
  58. package/src/host-file-reference.js +16 -0
  59. package/src/host-files.js +148 -0
  60. package/src/host-frame-budget.js +310 -0
  61. package/src/host-goals.js +284 -0
  62. package/src/host-history-view-limits.js +22 -0
  63. package/src/host-history-view.js +417 -0
  64. package/src/host-input-queue.js +505 -0
  65. package/src/host-media-fetch.js +511 -0
  66. package/src/host-media.js +678 -0
  67. package/src/host-models.js +231 -0
  68. package/src/host-palette.js +111 -0
  69. package/src/host-push-channels.js +37 -0
  70. package/src/host-reconnect.js +54 -0
  71. package/src/host-routes.js +266 -0
  72. package/src/host-settings.js +41 -0
  73. package/src/host-status.js +216 -0
  74. package/src/host-transfer-diagnostics.js +50 -0
  75. package/src/host.js +2497 -0
  76. package/src/login.js +31 -0
  77. package/src/logout.js +3 -0
  78. package/src/projection-read-model-sink.js +6 -0
  79. package/src/projection.js +43 -0
  80. package/src/session-flags.js +160 -0
  81. package/src/session-publisher.js +123 -0
  82. package/src/session-read-model.js +6 -0
@@ -0,0 +1,77 @@
1
+ # Projection read probe
2
+
3
+ How the projection *read* path behaves against a live Host, and the one defect
4
+ the probe found. The read path is the only part of the controller surface that
5
+ must never wake a session: `maker:goal:get-status` and
6
+ `maker:input:get-projection` are answered from state folded out of
7
+ `sessionController.control()`, and `local-db:sessions:list` is the phone's
8
+ device-responsiveness probe.
9
+
10
+ ## Running the probes
11
+
12
+ They talk to the loopback selftest route of a running smoke Host (README, *Safe
13
+ isolated DSH smoke test*) and write nothing outside `.sandbox/`:
14
+
15
+ ```powershell
16
+ pwsh -NoProfile -File .sandbox/probe-projection-read.ps1 # reads + the goal round trip
17
+ pwsh -NoProfile -File .sandbox/probe-queue-round-trip.ps1 # read -> enqueue -> read
18
+ pwsh -NoProfile -File .sandbox/probe-first-send.ps1 # create -> maker:send -> answer
19
+ pwsh -NoProfile -File .sandbox/probe-projection-frames.ps1 # does a live frame land at all
20
+ ```
21
+
22
+ The round-trip probes send real prompts to a throwaway session they create
23
+ themselves, or to the `session-selftest-*` session, never to the user's own
24
+ transcript.
25
+
26
+ ## Measured on a live Host
27
+
28
+ | Channel | Observed |
29
+ | --- | --- |
30
+ | `local-db:sessions:list` | 8–11 ms, two rows, full controller row shape |
31
+ | `maker:goal:get-status` | 1–2 ms |
32
+ | `maker:input:get-projection` | 1–3 ms |
33
+ | `maker:input:enqueue` | 5–8 ms, and the answer already carries the accepted item |
34
+ | `maker:send` on a fresh session | 5–7 ms accepted; prompt in the log within 1.5 s |
35
+
36
+ Reading a projection does not resume a session: a cold session's `running` flag
37
+ and `updatedAt` were both unchanged after a burst of goal and queue reads, and
38
+ its goal status stayed `null`.
39
+
40
+ The enqueue round trip closes in order. The Host's answer carries the accepted
41
+ item because it is folded from local state, which proves the **write**; the next
42
+ read no longer has it, which proves the **stream** — nothing else writes that
43
+ state, so only DSH's own queue frame can have replaced it. The marker prompt then
44
+ appears in the session log and the session answers it.
45
+
46
+ ## Defect found: the goal read ignored the stream baseline
47
+
48
+ `sessionController.control()` is *a baseline followed by replacements*, and a
49
+ replacement frame only follows a **change**. `createProjectionTracker` in
50
+ `src/host-goals.js` matched `frame.type === 'projection'` and nothing else, so
51
+ the entire baseline was discarded — including every goal that existed before the
52
+ Host connected, or that was created while it was disconnected. The controller
53
+ showed "no goal" for a goal that existed, and no live frame would ever correct
54
+ it, because the goal had not changed since.
55
+
56
+ The probe showed the symptom before the cause: `maker:goal:set` was accepted and
57
+ the write returned an active goal, while six consecutive
58
+ `maker:goal:get-status` reads over 4.2 s all returned `null`. The sibling tracker
59
+ for the input queue (`src/host-input-queue.js`) already consumed the baseline,
60
+ which is what made the omission in the goal tracker visible.
61
+
62
+ `apply` now folds the baseline — `frame.value.projections[sessionId].values.goal`
63
+ — and **replaces** the folded state rather than merging into it, so frames from a
64
+ previous stream generation cannot survive a reconnect. Three tests in
65
+ `test/host-goals.test.js` cover it: the baseline carries a goal, the baseline
66
+ clears stale state, and a baseline without a projection block neither throws nor
67
+ invents a goal. After the Host restarted with the fix, the same probe read the
68
+ goal back 400 ms after the write.
69
+
70
+ ## Contract note
71
+
72
+ `toGoalStatusPayload` distinguishes `undefined` ("never fetched") from `null`
73
+ ("fetched, no goal"), but the channel replies with `goalStatus(sessionId) ?? null`,
74
+ so the wire never carries the distinction: unknown and absent look identical. That
75
+ is deliberate — a relay reply has to carry a value — but it means the
76
+ "never fetched" branch in the mapper is only observable in unit tests, not by the
77
+ controller.
@@ -0,0 +1,259 @@
1
+ # 插件发布指南(DSH plugin bundle)
2
+
3
+ 本文说的"发布"是**把这个 DSH 插件做成别人(或别的 profile、别的机器)能装上的东西**。
4
+ 它的载体是 npm 包 + 一个 bundle 补丁层;git 提交与推送只是前置,不是发布。
5
+
6
+ ---
7
+
8
+ ## 1. 发布物是什么
9
+
10
+ 一个 DSH 插件 bundle 由**三样东西**定义,缺一不可:
11
+
12
+ | 位置 | 字段 | 作用 |
13
+ |---|---|---|
14
+ | `package.json` | `dsh.bundle.patch: ./cordis.patch.yml` | 声明自己是 bundle,并指向补丁层 |
15
+ | `cordis.patch.yml` | 插件行(见下) | 装载进 profile 的**那一条** |
16
+ | `package.json` | `main` / `exports["."]` / `exports["./client"]` | 服务端入口与**客户端卡片**入口 |
17
+ | `package.json` | `dsh.client.platform: "web"` | 卡片在 Web 面渲染;缺 `exports["./client"]` 时 DSH 会解析失败(ADR-0006) |
18
+
19
+ 本仓库当前的行:
20
+
21
+ ```yaml
22
+ - insert:
23
+ - id: dsh-cindy-host
24
+ name: dsh-cindy-host
25
+ inject: [settings]
26
+ ```
27
+
28
+ `insert`(而不是覆盖)意味着它**叠加**在基础 profile 上;`inject: [settings]` 是它唯一硬依赖的服务——
29
+ 其余能力(`sessionController`、`fs`、`webServer`…)都是**运行时探测**:有就服务,没有就拒绝并给出准确的
30
+ 错误码。这是插件能在各种 profile 下"挂得上"的关键,也是发布前必须回归的一点。
31
+
32
+ ## 2. 别人怎么装上它(三种消费方式)
33
+
34
+ ```bash
35
+ # a) 本地目录(开发期,link 语义)
36
+ dsh plugin --profile <profile> add <本仓库路径>
37
+
38
+ # b) tarball(不发 registry 也能分发)
39
+ npm pack # 产出 dsh-cindy-host-<version>.tgz
40
+ dsh plugin --profile <profile> add ./dsh-cindy-host-<version>.tgz
41
+
42
+ # c) registry(**0.1.2 起这是正式渠道**)
43
+ dsh plugin --profile <profile> add dsh-cindy-host@<version>
44
+ ```
45
+
46
+ 装完后 profile 的 `package.json` 里会出现 `dsh.profile.bundles` 含 `dsh-cindy-host`,
47
+ 依赖形态分别为 `link:` / `file:` / 版本区间。**生效仍然需要重启 `dsh web`**(见 `doc/releasing.md` §4)。
48
+
49
+ ## 3. 发布前的硬前提
50
+
51
+ 1. **必须在没有 `apiProxy` 的 profile 上挂得上**。当前 DSH Web(`0.1.5-rc.2`)不再挂 `apiProxy`,
52
+ 改由 Typert remotes 暴露会话;本插件在缺席时安全挂载并在通道层回 `NOT_AVAILABLE`。发布前用
53
+ 一个干净 profile 复核这一点(`dsh --profile <p> --dump-config` 能 compose、`dsh --profile <p>`
54
+ 起得来且没有 Cindy 登录提示)。
55
+ 2. **DSH 自己的包一律 `peerDependencies`,绝不进 `dependencies`(也绝不进
56
+ `optionalDependencies`)**。宿主提供它们;插件自己装一份,就等于把**另一代 DSH** 装进别人的
57
+ profile,宿主再组合这个混合体便起不来。0.1.1 犯的正是这个错,实测与规则见 §5.4 / §5.6。
58
+ `dependencies` 里只放宿主不提供的库(本包是 `keytar`、`ws` 与固定版本的 `werift`)。
59
+ 3. **原生依赖要写清**:`keytar` 是原生模块(Windows 凭据管理器)。发布说明里必须写明平台/Node ABI
60
+ 要求(本包 `engines.node >= 22`),否则会出现"装上了但起不来"。
61
+ 4. **`private` 必须是 `false`**(发布到 registry 的必要条件;它是防止误发布的开关,不是发布配置。
62
+ 0.1.2 起本包为 `false` + `license: MIT`,仓库根有 `LICENSE`)。
63
+ 5. **包内容必须是白名单**,而不是"整个目录"。`.sandbox/`(含凭据与会话)、`.claw/runtime`、验收日志、
64
+ `.claw/tasks` 的开发记录**都不该进包**——它们在 git 里是资产,在 npm 包里是负担与风险。
65
+
66
+ 建议的 `files` 白名单:
67
+
68
+ ```json
69
+ "files": ["src", "lib", "cordis.patch.yml", "README.md", "CHANGELOG.md", "doc/"]
70
+ ```
71
+
72
+ (`package.json` 里的实际白名单和这里一致;`test/`、`tools/`、`.claw/` 留在仓库里,需要随包分发
73
+ 测试时再加。)
74
+
75
+ ## 4. 版本与兼容性声明
76
+
77
+ - 语义化版本;插件对 DSH 的**兼容区间**要写进 README 顶部与 CHANGELOG:例如"验证于 DSH
78
+ `0.1.5-rc.2`"。
79
+ - 每次发布记录**验证过的 DSH 版本**,因为 DSH 的公开 API 仍在 rc 阶段滚动。
80
+ - 破坏性变更(例如撤下 `history-view-v1` 能力、改变通道语义)必须同时更新 README 的兼容性边界与
81
+ `.claw/truth` 里的对应条目。
82
+
83
+ ## 5. 发布步骤
84
+
85
+ ```
86
+ [ ] 1. 门禁:npm test / npm run audit:channels / 沙盒 acceptance / 真机 acceptance --with-prompts 105/105
87
+ [ ] 2. 版本号 + CHANGELOG(写清验证过的 DSH 版本与本轮用户可见变化)
88
+ [ ] 3. package.json:private=false、files 白名单、dependencies 里没有内部路径依赖
89
+ [ ] 4. npm pack --dry-run → 人眼过一遍文件清单(不得出现 .sandbox/、凭据、日志、.claw/runtime)
90
+ [ ] 5. 干净 profile 安装验证:Gate 1 compose 通过;Gate 2 起得来、无登录提示、卡片作为
91
+ window.__DSH_BOOT__ 的 dsh-cindy-host 条目被服务
92
+ [ ] 6. 发布:npm publish(或私有 registry / 仅发 tarball)
93
+ [ ] 7. 装到真机 profile → 按 doc/releasing.md §4 重启(先告知、留 60s)→ 真机复测本轮针对的现象
94
+ [ ] 8. 打 tag 并把 tag 推到远端,便于回滚与追溯
95
+ ```
96
+
97
+ 第 5 步是**唯一能证明"别人装得上"的一步**,不能省:本地 link 装法掩盖掉的白名单、原生依赖、
98
+ 入口导出问题,都会在这一步暴露。
99
+
100
+ ### 5.1 干净安装实际踩到的两个坑(2026-09-17,0.1.0)
101
+
102
+ 1. **可选能力被静态 import 变成硬要求**。`src/dsh-plugin.js` 顶层 `import` 了
103
+ `@deepseek-ai/dsh-host-apiproxy`(只有老 DSH 的 `apiProxy` 路径才用到),它的传递依赖
104
+ `@deepseek-ai/dsh-agent-presets` 在干净环境里解析到没有 `InvalidPresetIdError` 的副本,
105
+ **插件加载失败 → 整个 profile 起不来**。修法:改惰性 `require` + 失败降级,依赖移入
106
+ `optionalDependencies`。**规则:可选能力一律不得阻断 mount。**
107
+ 2. **原生依赖在 pnpm 10 下不会自动构建**。`keytar` 需要编译 `keytar.node`,而 pnpm 10 默认
108
+ **不执行依赖的构建脚本**,于是干净 profile 里没有二进制,顶层 `import keytar` 直接让插件加载
109
+ 失败。修法同上(惰性 + 降级:读凭据回"无会话",写凭据抛出 `CREDENTIAL_STORE_UNAVAILABLE`)。
110
+
111
+ ### 5.2 安装方须知(写进面向用户的说明)
112
+
113
+ **这个包在安装时需要编译一个原生模块**,这不是可选项,是它的依赖决定的:
114
+
115
+ - `keytar` 是**原生模块**(Windows 凭据管理器 / macOS Keychain / Linux Secret Service),
116
+ 安装时必须编译出 `keytar.node`。**原因**:Cindy 的登录会话必须存放进操作系统的凭据库,
117
+ 而不是明文写进配置或仓库——这是本插件的凭据存储方式,没有纯 JS 的等价物。
118
+ - 因此安装需要**编译工具链**:Windows 上通常是 VS Build Tools(含 C++ 工作负载),
119
+ macOS 需要 Xcode Command Line Tools,Linux 需要 `libsecret-1-dev` 等。
120
+ - **pnpm 用户**还必须显式允许构建脚本(pnpm 10 默认不跑依赖的构建脚本):
121
+ `pnpm approve-builds`,或在 profile 的 `package.json` 里加
122
+ `"pnpm": { "onlyBuiltDependencies": ["keytar"] }` 后重装。npm 默认会跑构建脚本。
123
+ - **编译失败也能装上**(0.1.1 起):插件照常 mount,只是登录/凭据相关能力明确不可用。
124
+ 换句话说,**"要编译"只影响凭据能力,不影响插件能否挂载**。
125
+
126
+ ### 5.3 测 tarball 时的一个陷阱
127
+
128
+ **同名同版本的 `file:` 依赖,pnpm 不会刷新**:修改后重新 `npm pack`、再 `dsh plugin add` 同一个
129
+ `dsh-cindy-host-demo-0.1.0.tgz`,装上去的还是**旧副本**(实测:tarball 里已是新代码,profile 里仍是
130
+ 旧代码)。要么先删掉 `profiles/<p>/node_modules/dsh-cindy-host-demo` 再装,要么——更贴近真实发布
131
+ 流程——**每次测试都升版本号**。
132
+
133
+ ### 5.4 全新 profile 起不来:根因是我们把旧一代 DSH 装进了它的依赖树(0.1.1 的缺陷,2026-09-17 更正)
134
+
135
+ **这一节原先的结论是错的,错误本身值得留着。** 当时写成「新建 profile 会回落到 DSH 嵌套依赖,
136
+ 是 DSH 侧的问题,与本包无关」,理由是失败栈里出现的是 DSH 自己的包。实际上:
137
+
138
+ ```
139
+ @deepseek-ai/dsh-client-file-upload
140
+ ctx.commands.registerFileReceiptResolver is not a function
141
+ ```
142
+
143
+ 失败栈里出现 DSH 的包**不代表** DSH 有问题——它也可能是我们把它**换成了另一代**。真正发生的是:
144
+
145
+ 1. `@deepseek-ai/dsh-host-apiproxy@0.1.1-rc.2` 的依赖是 **28 个 `@deepseek-ai/*@^0.1.1-rc.2`**
146
+ (`dsh-commands`、`dsh-llm`、`dsh-session`、`dsh-tools`…)。它当时在 `optionalDependencies` 里,
147
+ 而 **pnpm 默认连 optionalDependency 也安装**,于是这一代被整体装进 profile。
148
+ 实测:安装前 profile-local `@deepseek-ai` 为 0,安装后 21 个,全部 `0.1.1-rc.2`。
149
+ 2. 宿主是 `0.1.5-rc.2`。组合里于是混了两代:`file-upload` 拿到的是另一代的 `ctx.commands`
150
+ (缺 `registerFileReceiptResolver`);`loader entry session (@deepseek-ai/dsh-session)` 里
151
+ 0.1.1 的 `dsh-session` 去 import 宿主的 0.1.5 `dsh-llm`,后者没有 `CallId`。
152
+ 3. 我们之所以把旧代声明成依赖,是因为**代码 import 了只在旧代存在的导出**:
153
+ `settingsNamespace` 存在于 `@deepseek-ai/dsh-settings@0.1.1-rc.2`,而 `0.1.5-rc.2` 的导出面只有
154
+ `SettingsProvider / SettingsConflictError / redactSecrets / default`(直接 import 验证过)。
155
+ 也就是说插件一直靠"把旧那代装进来"才能加载。
156
+
157
+ **0.1.2 的修法**:把 `settingsNamespace` 内联进 `src/dsh-plugin.js`(它的实现只是个 brand:
158
+ 校验 `/^[a-z][a-z0-9-]*$/` 后原样返回字符串),DSH 的包全部改 `peerDependencies`,
159
+ `dsh-host-apiproxy` 另标 `optional`。修完的实测(全新 `DSH_HOME` + `--from-default-profile web`):
160
+
161
+ ```
162
+ add exit=0;profile-local @deepseek-ai = 0
163
+ compose exit=0(有 dsh-cindy-host 行)
164
+ dsh web: http://127.0.0.1:3095/?token=…
165
+ /api/dsh-cindy-host/status → 200 installed=true state=disconnected dataSource=session-controller
166
+ ```
167
+
168
+ **教训(比结论更值钱)**:
169
+
170
+ - 失败栈里出现别人的包,**先问"这个包是从哪解析来的、是哪一代"**,再问"谁的锅"。我们当时只看了
171
+ 包名,就把它判给了 DSH。
172
+ - `optionalDependencies` **不是**"可以不装":pnpm 默认会装。想表达"宿主有就用、没有就算了",
173
+ 正确写法是 `peerDependencies` + `peerDependenciesMeta.optional`。
174
+ - 一个**只在一个方向成立的现象**("`file:` 装法坏、`link:` 装法好")要当心:这里的真变量不是
175
+ 安装形式,而是 pnpm 有没有**真的**把依赖图装进 profile。第一次改回 `link:` 时 pnpm 打了
176
+ `resolved 71, reused 1, added 0`(什么也没干),所以那次"好"是假象。
177
+
178
+ ### 5.5 发行渠道的历史:0.1.1 走 GitHub Release,0.1.2 起走 npm registry(2026-09-17)
179
+
180
+ **0.1.1(历史;附件有缺陷,已删除)**:当时按上面第 6 步的"仅发 tarball"分支执行——发 GitHub Release
181
+ `v0.1.1` 并把 `dsh-cindy-host-demo-0.1.1.tgz` 作为附件,同时打 tag 推远端。选它的理由是当时
182
+ `license` 还是 `UNLICENSED`(公开进 registry 等于以未授权状态分发),而"干净 profile 冷启动"那一层
183
+ 又失败(后来查明是**我们自己的依赖污染**,见 §5.4)。**那个附件会让装它的 profile 起不来,已经删掉**
184
+ (下载地址返回 `404`);`v0.1.1` 的 Release 页与 tag 保留作追溯,页面正文写的是"本版本已撤回,请用
185
+ npm 的 0.1.2"。
186
+
187
+ **0.1.2(当前渠道:npm registry)**:`private: false` + `license: MIT` + 仓库根 `LICENSE`,然后
188
+
189
+ ```bash
190
+ npm publish --access public # → dsh-cindy-host-demo@0.1.2
191
+ npm view dsh-cindy-host-demo version # 读回来确认(刚发布时会有几十秒的复制延迟,404 不等于失败)
192
+ ```
193
+
194
+ 安装方向就变成按包名(§2 的 c):
195
+
196
+ ```bash
197
+ dsh plugin --profile <profile> add dsh-cindy-host-demo@0.1.2
198
+ ```
199
+
200
+ 实测(全新 `DSH_HOME` + `--from-default-profile web`):`add` exit 0、profile-local
201
+ `@deepseek-ai` = 0、`dsh web` 启动、`/api/dsh-cindy-host/status` → `200 installed=true`。
202
+ `registry` 上读回的 `dependencies` 只有 `keytar, ws`(§5.6 规则一的直接证据)。
203
+
204
+ **撤版方式也随之改变**:见 §6。
205
+
206
+ ### 5.6 依赖规则:DSH 的包只能是对等依赖(写死的检查)
207
+
208
+ **规则一**:`dependencies` 里不出现任何 `@deepseek-ai/*`。装完后在 profile 里查:
209
+
210
+ ```powershell
211
+ (Get-ChildItem "$env:DSH_HOME\profiles\<p>\node_modules\@deepseek-ai" -Directory -ErrorAction SilentlyContinue).Count
212
+ # 期望 0 —— 非 0 就是我们把某个 DSH 代装进了别人的树
213
+ ```
214
+
215
+ **规则二**:`peerDependencies` 的区间必须覆盖宿主当前那一代。semver 的预发布规则下
216
+ `^0.1.1-rc.2` **匹配不到** `0.1.5-rc.2`(比对符的 `[major,minor,patch]` 必须一致),所以要写
217
+ `^0.1.1-rc.2 || ^0.1.5-rc.2` 这样的或区间。生态里的参照:
218
+ `dsh-codex` 用 `^0.1.5-rc.2`,`dsh-cost-meter` 用
219
+ `^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0`。
220
+
221
+ **规则三**:`import` 的每一个具名导出,都要在当前宿主上验一遍。发布前跑一次:
222
+
223
+ ```powershell
224
+ dsh --profile <干净 profile> --dump-config # compose 必须 exit 0 且含我们的行
225
+ dsh --profile <干净 profile> --no-open --port <port>
226
+ # 然后 GET /api/dsh-cindy-host/status → 200
227
+ ```
228
+
229
+ 只做 compose 不够:**缺一个导出同样能让 compose 通过**,它到加载时才炸,而且 DSH 把加载失败的
230
+ entry 当作致命错误(整个 profile 起不来)。`0.1.1` 就是 compose 通过、加载失败。
231
+
232
+ ## 6. 回滚与撤版
233
+
234
+ - **未发 registry(tarball 分发)**:装回上一个 tarball 即可,最干净。0.1.1 的实例正是这条:
235
+ 它是 GitHub Release 附件而不是 registry 版本,所以"撤版"只需要一条说明加一个修复版本
236
+ (0.1.2),不需要动 registry。
237
+ - **已发 npm**:`npm deprecate dsh-cindy-host@<bad> "原因"` 并立刻发一个修复版本;
238
+ `npm unpublish` 有 72 小时窗口且被 registry 策略限制,不要把它当成回滚方案。
239
+ - **插件侧**:`dsh plugin --profile <p> add dsh-cindy-host@<上一个版本>` + 重启。
240
+ - 回滚后仍要留下痕迹:`.claw/adr` 里追加 `Decision evolution`(为什么发坏了、下次怎么防)。
241
+
242
+ ## 7. 检查清单(可直接复制)
243
+
244
+ ```
245
+ [ ] 门禁四项全绿(unit / audit / 沙盒 / 真机 105-105)
246
+ [ ] 版本号已升,CHANGELOG 写明验证过的 DSH 版本
247
+ [ ] private=false;license 是 MIT 且有 LICENSE 文件;files 白名单
248
+ [ ] dependencies 里没有任何 @deepseek-ai/*(§5.6 规则一)
249
+ [ ] npm pack --dry-run 清单已人眼确认(无凭据/沙盒/日志/开发记录)
250
+ [ ] 干净 DSH_HOME + web 派生 profile 装 tarball:add exit 0
251
+ [ ] 装完 profile-local @deepseek-ai 计数 == 0(没把某一代 DSH 拖进去)
252
+ [ ] compose 通过 **并且真的启动**,GET /api/dsh-cindy-host/status → 200 installed=true
253
+ (只做 compose 不算数:缺一个具名导出同样能 compose 通过,却在加载时让整个 profile 起不来)
254
+ [ ] 发布:`npm publish --access public`,再用 `npm view dsh-cindy-host version` 读回确认
255
+ (刚发布后几十秒内可能 404,那是复制延迟,不是失败);同时打 tag 推远端
256
+ [ ] **从 registry 再装一次**到干净 home 并启动成功(发布渠道自己也要走一遍验收,不能只验本地 tarball)
257
+ [ ] 真机:告知 → 60s → 重启 → 复测现象
258
+ [ ] 失败路径已想好(deprecate + 修复版本,而不是 unpublish)
259
+ ```
@@ -0,0 +1,149 @@
1
+ # 发布指南
2
+
3
+ 本仓库的"发布"不是打 tag 推包,而是**把一次改动安全地送上真机并留下可复查的痕迹**。下面这条链路
4
+ 是今天一整天(39+ 次提交、多次真机复测)实际走出来的顺序,按它走可以避免重复踩坑。
5
+
6
+ 适用:`dsh-cindy-host`(DSH ⇄ Cindy 手机客户端桥接插件)。
7
+
8
+ ---
9
+
10
+ ## 0. 一句话流程
11
+
12
+ ```
13
+ 隔离验证(沙盒) → 门禁全绿 → 用 claw 留证(plan/task/truth/ADR) → 脱敏 → 部署(重启才是生效点)
14
+ ```
15
+
16
+ **核心判断**:改动是否"生效"只取决于 `dsh web` 有没有重启过。代码合并、文件保存、测试通过
17
+ **都不等于生效**。
18
+
19
+ ---
20
+
21
+ ## 1. 发布前门禁(四项,必须全绿)
22
+
23
+ | # | 命令 | 期望 | 说明 |
24
+ |---|---|---|---|
25
+ | 1 | `npm test` | 全绿 | 单元与契约测试;任何一项失败都不发布 |
26
+ | 2 | `npm run audit:channels` | `unclassified: 0` | 每个通道要么被服务、要么被显式拒绝,不允许"没分类" |
27
+ | 3 | `npm run sandbox` + `npm run acceptance:sandbox` | 沙盒 80+/81(唯一允许失败:未连中继) | 隔离实例验证,不打扰用户 |
28
+ | 4 | `npm run acceptance -- --with-prompts` | **105/105**、52/52 served、10/10 push | 对**真机**的端到端验收(会发真实 prompt) |
29
+
30
+ 第 3 项是这一轮新增的能力:`tools/sandbox.mjs` 起一个**独立 DSH_HOME + 独立凭据 + 独立中继设备**的
31
+ 第二实例(插件条目是指向本仓库的 link),所以功能迭代可以在那里反复重启验证,不必每次打扰用户。
32
+ 它的局限也要记住:**会话少、规模小**,规模类问题(219 会话冷启动、超长转录)只能在真机验。
33
+
34
+ ---
35
+
36
+ ## 2. 用 claw 组织这次发布
37
+
38
+ claw 的价值不是流程仪式,而是**让"做过什么、依据是什么"在会话结束后仍然可查**。发布这种跨多轮、
39
+ 多证据的活尤其需要它。
40
+
41
+ ```bash
42
+ # 主代理侧(DSH 里一律走 claw_run,不直接跑 CLI)
43
+ claw_run plan.create { title, goal, scope: "session" }
44
+ claw_run plan.start { requirements, acceptance: [...], add_tasks: [{title, detail}, ...] }
45
+ claw_run task.done { id }
46
+ claw_run plan.done { retrospective, key_decisions, what_worked, issues, follow_ups }
47
+ ```
48
+
49
+ 三条纪律:
50
+
51
+ 1. **每个 `task.done` 之前,前一条消息里要给出证据**(测试数、真机时间戳、日志片段)。`task.done`
52
+ 本身不接受结论字段——结论写在消息里。
53
+ 2. **plan.start 的 `acceptance` 就是发布的验收标准**,发布时逐条对照,不凭感觉。
54
+ 3. **计划文件不在仓库里**(写在 `~/.claw/runtime/sessions/<session>/tasks/...`);仓库里的
55
+ `.claw/{truth,adr,tasks}` 才是要随代码发布的**项目记忆**。发布前确认:本轮新增的事实进了
56
+ `.claw/truth/*.md` 的 `<!-- state: current -->` 段,新增的**决策**进了 `.claw/adr/*.md`(含
57
+ `Decision evolution` 段,写清"当初为什么这么定、后来为什么改")。
58
+
59
+ 判断哪些结论值得进项目记忆,用这一条:**"如果下一个接手的人不知道这件事,他会不会重犯?"**
60
+ 今天写进 truth 的三类都是这个标准:手机端"切后台回来入站失效"的归属与一分钟复现法、turn 状态的
61
+ 权威来源、以及"通知不是用户消息"这类投影决策。
62
+
63
+ ---
64
+
65
+ ## 3. 脱敏(只要仓库会公开,就必须做)
66
+
67
+ 顺序很重要,**今天在这里出过一次事故**:
68
+
69
+ ```bash
70
+ # 1) 先扫(两类都要扫)
71
+ git grep -I -E 'sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|BEGIN [A-Z ]*PRIVATE KEY'
72
+ # 以及环境痕迹:手机型号、设备 UUID、用户名、本机路径、手机号、账号 id
73
+
74
+ # 2) 替换 + 重写历史(新建提交删不掉旧痕迹)
75
+ git filter-branch -f --tree-filter "node .sandbox/redact.mjs ." -- --all
76
+ rm -rf .git/refs/original && git reflog expire --expire=now --all && git gc --prune=now
77
+ git push --force origin main
78
+
79
+ # 3) 验证:拉一份干净副本复扫(不要相信本地结论)
80
+ git clone --depth 1 <url> /tmp/verify && grep -r <pattern> /tmp/verify
81
+ ```
82
+
83
+ **两条教训(都来自今天)**:
84
+
85
+ - **要删/改名仓库,先把它改成 private,再动名字。** 重命名会**带走可见性**:我们先把旧仓库设成
86
+ public、之后才 rename,结果那个"待删除"的壳带着**未脱敏历史**公开了 1–2 分钟。
87
+ - **`gh repo delete` 需要 `delete_repo` scope**(默认的 `repo` 不够)。优雅做法:旧仓库**先改私有**,
88
+ 再用网页 Danger Zone 删除(不需要额外 scope),别为了删除去改 token 权限。
89
+
90
+ ---
91
+
92
+ ## 4. 部署到 DSH(重启才是生效点)
93
+
94
+ 插件以 bundle 形式挂在 profile 里(`link:` 指向本仓库,或 `cordis.patch.yml` 覆盖行)。**改完代码必须
95
+ 重启 `dsh web` 才生效**。重启有三条路:
96
+
97
+ | 场景 | 做法 |
98
+ |---|---|
99
+ | 迭代/功能验证 | `npm run sandbox`(3081,独立实例)→ 我自己重启,不打扰用户 |
100
+ | 真机生效 | `node tools/restart-host.mjs --apply`(默认 60s 缓冲,通过 WMI 启动 supervisor;**先拿到用户同意**,见下) |
101
+ | **别的 agent / 别的项目** | 全局 skill **`dsh-restart`**:`~/.agents/skills/dsh-restart/scripts/restart-dsh.mjs`(同一套逻辑,去掉了仓库依赖;日志在 `~/.agents/logs/dsh-restart/`)。任何 agent 触发「重启 dsh」时都会用到它,规则同样遵守 ADR-0009 |
102
+
103
+ **重启的硬规则**(写进 ADR-0009):
104
+
105
+ 1. **必须先拿到明确同意,再 apply**——先用 `ask_user_question` 问,用户点了同意才执行 `--apply`。
106
+ 「告知」不等于「同意」,倒计时也不是同意:与掉线同时到达的提醒不算提醒。
107
+ 2. **必须先告知,并留足被读到的窗口**——默认 `--grace 60`。
108
+ 3. **`--apply` 会先清扫整棵 dsh 进程树**(含命令子进程/沙箱线程和上次留下的孤儿),再启动新实例。
109
+ 这是"干净重启"换来的,代价说清楚:**本机每个 dsh 实例的命令子进程都会被停**,别的会话正在跑的命令
110
+ 也会跟着死。不想动就加 `--keep-sandboxes`(只停目标那棵树)。执行前先跑一次干运行,读它打出的
111
+ `cleanupPids` 名单。
112
+ 4. 重启会**杀掉正在跑的那一轮**;会话日志是持久的,但**没有任何东西会自动续上**——需要一条新消息
113
+ 唤醒(手机发最稳,因为它不依赖网页 token)。
114
+ 5. 新实例会自己打开新标签页(旧页面的 token 失效)。
115
+ 6. 失败模式:**DSH 停在那里**,只有人能手动起。日志在 `.sandbox/host-restart.log`(全局 skill 那条路写在
116
+ `~/.agents/logs/dsh-restart/host-restart.log`,新实例自己的输出在 `dsh-web.log`,新 token 在其最后一行)。
117
+
118
+ 重启后自检三件事:`/status` 的 `boundaries`(`recovered`/`starting`/`silent`)、`handlerErrors`(应为空
119
+ 或已归因到具体通道)、以及本轮新增诊断字段是否在场。
120
+
121
+ ---
122
+
123
+ ## 5. 回滚
124
+
125
+ - 代码:`git revert <sha>`(**不要 force push 已公开的历史**)→ 走第 1 节门禁 → 第 4 节重启。
126
+ - 真机出问题时的最快止损:`node tools/restart-host.mjs --apply` 重启本身就能清掉运行态故障;
127
+ 要回到上一版就先 `git checkout <上一版> -- src/ && 重启`,再正式 revert。
128
+ - 诊断面永远是第一手证据:`/status` 的 `invokeTotals` / `refusalTotals` / `recentInvokes`(带 src 归属)
129
+ / `recentPushes`(带 watchers)/ `handlerErrors`(`where` 带通道名)/ `boundaries` / `suppressedNotices`。
130
+
131
+ ---
132
+
133
+ ## 6. 发布检查清单
134
+
135
+ ```
136
+ [ ] npm test 全绿
137
+ [ ] npm run audit:channels → unclassified 0
138
+ [ ] npm run sandbox && npm run acceptance:sandbox
139
+ [ ] 真机 acceptance --with-prompts → 105/105
140
+ [ ] 本轮事实进了 .claw/truth(state: current)
141
+ [ ] 本轮决策进了 .claw/adr(含 Decision evolution)
142
+ [ ] 若公开仓库:密钥 + 环境痕迹扫描 → 需要则 filter-branch + 干净副本复扫
143
+ [ ] 推送前确认没有把 .sandbox / 凭据 / 日志带进版本库
144
+ [ ] 部署:告知 → 留 60s → 重启 → 自检 boundaries/handlerErrors
145
+ [ ] 用真机复测本轮改动所针对的现象(不是只跑测试)
146
+ ```
147
+
148
+ 最后一条是最重要的:**发布是否成功,由真机现象决定,不由测试数决定**。今天所有真正的缺陷都是
149
+ 测试全绿时用户在手机上发现的。
@@ -0,0 +1,24 @@
1
+ # Research: DSH Cindy Host
2
+
3
+ ## Objective
4
+ Build a read-only host-side demo that projects a DSH conversation list and safe activity summaries. A future Cindy transport may consume the projection, but this demo does not connect to Cindy.
5
+
6
+ ## Verified findings
7
+ - DSH host plugins receive `ctx.apiProxy` through `@deepseek-ai/dsh-host-apiproxy`.
8
+ - The same package exports `InProcessApiClient` and `toFetchHandler`, so a host bundle can read sessions without browser scraping or a network carrier.
9
+ - The DSH events API exposes host and mux streams. Mux may include raw session events, messages, tool views, approvals, and questions.
10
+ - Cindy uses a host-as-source-of-truth model; controller clients consume projected list/activity views.
11
+
12
+ ## Scope for this demo
13
+ Included: session snapshot, session lifecycle/running-state summaries, stale/rebaseline handling, console presentation, deterministic fixtures and tests.
14
+
15
+ Excluded: Cindy relay/authentication, Android integration, remote commands, approval responses, file access, settings, tokens, raw message content, tool payloads, error text, and any DSH/Cindy source modification.
16
+
17
+ ## Evidence anchors
18
+ - `@deepseek-ai/dsh-host-apiproxy/lib/types/index.d.ts` lines 2-8 and 24-27: host `ctx.apiProxy` and transport-neutral gateway.
19
+ - `@deepseek-ai/dsh-host-apiproxy/lib/types/api/events.d.ts`: host/mux frame contracts and answerable interaction frames.
20
+ - `@deepseek-ai/dsh-host-apiproxy/lib/types/fetch/client.d.ts`: in-process client.
21
+ - `G:/Projects/Cindy/packages/device-link/src/topics.ts`: controlled host projection/topic model.
22
+
23
+ ## Decision
24
+ Use an internal `ProjectionSink` rather than Cindy device-link. It keeps this demo executable without credentials and makes the later transport integration an explicit security-reviewed change.
@@ -0,0 +1,67 @@
1
+ # 助手会话在手机默认列表缺失:2026-09-20 排查
2
+
3
+ ## 状态与结论
4
+
5
+ 未完成真机验收;后续操作前只读检查发现目标已为 active,持久归档条目已消失(非本修复会话执行恢复);计划继续等待真机验收。
6
+ 本轮仅增加回归与本文档,没有业务源码补丁,没有改动宿主设置、安装包、Cindy 或 DSH 核心,没有重启。
7
+
8
+ 直接原因证据是 **Host 按稳定 ID 将目标会话投影为 archived**,并非 Host 没发现它。尚不能归因于误操作、客户端发错 patch 或改名;没有归档写入时刻/操作者证据。也不能据此排除恢复后仍存在独立手机传输问题。
9
+
10
+ 目标:session-b19a5c27-8bc6-4ba9-a2ea-4786ba88815f,工作区 G:\Projects\助手。
11
+
12
+ ## 脱敏运行证据
13
+
14
+ 2026-09-20 12:33 UTC 左右只读访问现行 http://127.0.0.1:3080/api/dsh-cindy-host/status:
15
+
16
+ - state=connected,dataSource=session-controller。
17
+ - recentInvokes 中 12:29:32.921Z 至 12:31:08.932Z 的成功 local-db:sessions:list 回复预览均包含目标 ID。
18
+ - 目标行 title=09-20,workingDir=G:\Projects\助手,status=archived,running=true;createdAt=2026-09-20T07:01:52.583Z。
19
+ - 12:31:08 的目标 updatedAt 已更新为 12:30:06.909Z:并非仅停留在旧标题/旧活动的列表。
20
+ - 当次 subscriptions.sessions=[]:没有目标的实时订阅。不能把 Host 回复成功当作手机收到。
21
+ - C:/Users/chany/.dsh/settings.yaml:281-282 是目标 ID 对应的 status: archived,未修改该文件。其他会话和凭据不复制到报告。
22
+ - 压缩会话文件首个解压帧的 header:同一 ID、cwd=G:\Projects\助手、createdAt=1789887712583、delegationDepth=0、isSeeded=false。仅检查首帧 header,**没有据此断言历史总量或完整性**。
23
+
24
+ 实际持久 header 与回复路径一致。目前无旧名变更审计,不能确认用户所说的改名具体对象及时间;当前标题确认为 09-20。
25
+
26
+ ## 代码证据与缓存边界
27
+
28
+ - src/dsh-session-source.js:43-46 仅过滤 subagent、blank、parentSessionId;:218-219 读取 controller 列表。:250-287 以 sessionId 为键关联标题与元数据,不按标题匹配。
29
+ - src/session-flags.js:63-76、133-156:flags 以 sessionId 为键;archived/deleted 被隐藏,标题无关。
30
+ - src/cindy-session-row.js:53-71:扁平 id 不变,归档状态写入手机行;既有客户端契约默认筛选 active。
31
+ - src/cindy-channels.js:959-1002:title-only patch 仅重命名;只有显式 status/pinnedAt 才写 flags。恢复用同 ID 的 status: active;写后失效行缓存。安装 profile 的同一代码段已只读核对一致。
32
+ - src/dsh-plugin.js:1388-1389:flags 通过 settings.mutate 的 set 整体替换,避免旧 merge 无法清除归档的问题。
33
+ - src/cindy-channels.js:569-604:get/history 使用 sessionId;:504-555:重订阅 session:<ID> 时推送 turn/input/history invalidation;不按标题恢复。
34
+ - 列表 stale-while-revalidate 和标题 TTL 可短暂显示旧标题;它们不改变 ID,也不制造 archived。未尝试清缓存。
35
+
36
+ ## 回归
37
+
38
+ 新增 test/session-identity-recovery.test.js,使用真实 source/router/flags 与注入服务,三条测试:
39
+
40
+ 1. 改名后同 ID、创建时间、历史读取 ID 不变;同名不同 ID 分离;旧订阅 topic 重连仍指向原 ID,并触发 input/history 补同步。
41
+ 2. 旧格式 archived + 空 pinnedAt 可读取;改名不自动取消归档;显式恢复清除缓存及持久标记,仅恢复目标而非同名另一会话。
42
+ 3. 外部标题改动在现有标题与行缓存 TTL 后收敛,ID/active 状态不变。
43
+
44
+ 相关四文件测试:94/94 通过。
45
+ npm test:556/556 通过,exit 0。
46
+ 这验证 Host 合同,不是真机验收,也不是生产历史完整性的证明。
47
+
48
+ ## 给助手的下一步(待协调,未执行)
49
+
50
+ 1. 告知用户该 ID 已归档而不是消失。经确认,在现有客户端归档视图对 **该 ID** 执行恢复(既有 patch-meta status: active),不要按“09-20”标题猜选,不改其他会话。
51
+ 2. 验证真实 Host 行变为 active、持久 flags 不再包含该 ID 的 archived。使用既有恢复路径通常不需部署或重启;本轮仅测试/文档无运行时代码需要发布。
52
+ 3. 真机确认默认列表出现该会话、旧历史可读、桌面/手机继续同步;后台返回或断线后重订阅仍是同一 ID;同名另一会话不串线。
53
+ 4. 如果归档视图无法恢复或恢复后仍丢失,收集恢复请求的脱敏 channel/ID/status、Host 回复与手机订阅证据,再决定最小业务补丁归属。不盲清数据库/配对,不先改 Cindy 或安装核心。
54
+
55
+ 当前没有真实手机操作能力,未执行上述验收。不要将本报告或测试通过描述为“已修复手机”。
56
+
57
+ ## 后续授权恢复:操作前状态已恢复
58
+
59
+ 收到助手安排后恢复计划,按要求先只读检查 archived 前置条件,没有直接编辑 settings,也没有提取凭据或尝试绕过认证。
60
+
61
+ - 现行 3080 status 的 recentInvokes 中,2026-09-20T12:41:48.89Z 和 12:41:52.649Z 两条真实 sessions:list 成功回复均记录目标 ID 为 **active**。
62
+ - 同一行 title 仍为 09-20,workingDir 仍为 G:\Projects\助手,createdAt 仍为 2026-09-20T07:01:52.583Z。
63
+ - 再次只读精确搜索 C:/Users/chany/.dsh/settings.yaml 中的完整目标 ID,结果为 0 条;原来 :281-282 的目标 archived 条目已不存在。未输出其他会话设置。
64
+ - 因“仍 archived”前置条件不成立,本修复会话没有发出 patch-meta 或其他写操作;避免重复恢复或覆盖并发操作。恢复操作者及具体写入来源未核实,不归功于本会话。
65
+ - 上述 Host 证据来自状态接口保存的实际列表回复,不是主动调用新的列表 RPC,也不是手机接收证据。未核验完整历史内容或手机继续同步。
66
+
67
+ Host 侧期望状态已观察到,无部署、重启或新增业务补丁需求。剩余最小用户动作是:在手机现有连接刷新/重进会话列表,打开这一精确 ID 对应会话,验证旧历史与继续同步;如仍不可用,保留具体症状和订阅/请求证据再继续排查。无需再次取消归档,更不要清配对或数据库。