dsh-deeppilot 0.9.16 → 0.9.17

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.
@@ -5,6 +5,29 @@
5
5
  > move the lower bound, at each `0.x` minor boundary, or when a host inside the
6
6
  > range breaks at runtime.
7
7
 
8
+ ## Runtime break — image read-back since `dsh-v0.2.2-alpha.2` (2026-11-06)
9
+
10
+ - **Symptom (issue #42):** every conversation image (tool results and the
11
+ user's own uploads) spins forever on the phone on `0.2.2-alpha.2` through
12
+ `0.2.2-rc.1`; `0.2.1-rc.5` is unaffected.
13
+ - **Cause:** `SessionController.attachment` changed its request from
14
+ `{ sessionId, attachmentId }` to `{ sessionId, attachment: ImageAttachmentRef }`.
15
+ The host now verifies the ref's `mediaType`/`bytes`/`width`/`height` against
16
+ the stored file and no longer checks that the session references the image
17
+ (`sessionId` is a gateway scope only). The bridge still sent the old shape, so
18
+ `readImage(undefined)` failed and the phone got `E_NOT_FOUND`; the iOS
19
+ thumbnail had no failure state. Earlier audits compared method names only, so
20
+ this signature change was missed — audit request shapes, not just presence.
21
+ - **Fix (plugin, no protocol change):** `src/image-ref-index.ts` remembers the
22
+ full host ref for every image the bridge projects (live events, tail, history
23
+ pages) and, on a miss, pages back through session history (at most 20 pages of
24
+ 500 messages). `HostBridge.attachmentData` sends `attachmentId` and
25
+ `attachment` together — the controller is injected directly, without schema
26
+ validation, so each generation reads its own field. A ref is only supplied when
27
+ that session projected the image, which restores the session-membership check
28
+ the new host dropped. `tests/dsh-api-proxy.test.ts` runs both host contracts.
29
+ - **Declaration:** unchanged; both generations are served.
30
+
8
31
  ## UI integration check — native settings controls (2026-11-05)
9
32
 
10
33
  - **Runtime surface:** `@deepseek-ai/dsh-client-ui-primitives` is an implicit
@@ -2376,3 +2399,40 @@ release notes.
2376
2399
  - **发布后核验。** 来源证明类型 `https://slsa.dev/provenance/v1`,`gitCommit` 为发布提交,工作流引用 `refs/tags/v0.9.13`,invocation 为 `40911590022`;tarball SHA1 `ca2c754e3421cddd14a795c5b4d869c5ea4b3cf9`、SHA512 与注册表一致,69 个条目、解包 131,263,175 字节。全新隔离 `DSH_HOME`(`/tmp/dsh-verify-0913`,pnpm 从 npm 安装,不是 link)在真实 `0.2.2-alpha.2` 宿主上启动,`https://127.0.0.1:3098/phone/health` 返回 200,`serverVersion` 为 `0.9.13`、`dataPlane: true`;GitHub Release 在这一步通过后才公开并标记 Latest。
2377
2400
  - **流程变更。** 用户决定保留暂存流程;批准步骤与「工作流末尾核对在批准前必然失败」已写入 `docs/NPM_PUBLISHING.md` 与 `AGENTS.md` 发布第 4–5 步。工作流本身未改:末尾核对在 staged 版本上仍会标红,是否改成「检测到 staged 即视为成功并输出待批准」留待下次发布前决定。
2378
2401
  - **局限。** 未做真机、Funnel 端到端、真实 `updateBundle`/HMR 回滚安装,也没有在 `0.2.1-rc.4` 上做真实业务回合;上述健康检查只证明插件能被宿主组合、启动并应答。
2402
+
2403
+ ## 2026-11-07 — 插件自更新真实宿主验证:HMR 在线替换失败,禁用 HMR 后成功
2404
+
2405
+ **环境。** `dsh 0.2.2-rc.1` / Node `24.22.1` / pnpm `12.8.1`;两套独立 `DSH_HOME`(`/tmp/dsh-plugin-update-verify/` 与工作区 `.verify-plugin-update/`,web 端口 3397/3497、插件 LAN TLS 3197/3297);`dsh-deeppilot@0.9.15` 以 registry 精确 spec 安装,目标 `0.9.16`。真实 WSS 鉴权(P-256 挑战/证明)驱动 wire 帧,非内存假件。
2406
+
2407
+ **结论:能力位与查询链路通,HMR 在线替换不通。**
2408
+
2409
+ | 项目 | 实测 |
2410
+ |---|---|
2411
+ | `welcome.capabilities.pluginUpdate` | `true`(rc.1 上首次实测,此前只在 alpha.2 记过) |
2412
+ | `c2s.plugin.update.check` | `currentVersion 0.9.15`、`latestVersion 0.9.16`、`updateAvailable true`、`compatible true`、`protocolCompatible true`;宿主 `listVersions('dsh-deeppilot')` 真机返回 `listed`,补上 `.feature-scout/FEATURES.md:145` 的未验证项 |
2413
+ | `c2s.plugin.update.apply`(HMR 开启) | pnpm 成功装 0.9.16,宿主 `installBundle` 返回 `application:'failed', stage:'enable'`,触发 `restore` 装回 0.9.15;`restore` 报同一错误。两次独立复现(不同 `DSH_HOME`、不同卷、不同请求编号) |
2414
+ | `c2s.plugin.update.apply`(禁用 HMR) | progress → `restart-required`;profile 与磁盘变 0.9.16;重启后 health `serverVersion 0.9.16`、journal `done`(「重启后已运行目标版本,更新完成」)、`check` 返回 `currentVersion 0.9.16 / updateAvailable false`。**完整闭环成功** |
2415
+
2416
+ **根因(宿主 `updateBundle` 完整返回,见 `.verify-plugin-update/diag-updatebundle.json`)。**
2417
+
2418
+ ```
2419
+ result.error / restore.error:
2420
+ code: operation-error
2421
+ diagnostic: dsh: warning: 1 entry did not activate
2422
+ deeppilot-directory-picker-browse (@deepseek-ai/dsh-host-directory-picker-browse):
2423
+ Error: service "directoryPicker" has been registered at <NativeDirectoryPicker>
2424
+ ```
2425
+
2426
+ `dsh-web-app` 的 `directory-picker` 行是 `-auto`,启动时按宿主打境用 `ctx.loader.create()` 把 native 或 browse 后端挂进内存根树,disposer 异步卸载。我方 bundle 用 `disabled: true` 禁用它、再 `insert` 一个不同 id 的 `-browse` 行。`updateBundle` 有 HMR 时走 `removeBundle → installBundle`:bundle 层被移除 → `-auto` 复活并挂 `NativeDirectoryPicker` → 重新加入 bundle 时 `-browse` 注册 `directoryPicker` 撞名 → 宿主按「1 entry did not activate」把整次包操作判失败并回滚。官方 `-auto` README:40 已写明「同时挂载选择器和某个后端行会明确报错(重复的 `directoryPicker` 服务)」。CLI 覆盖安装 + 重启是全新组合、无残留,0.9.16 能正常激活,故不是包或网络问题。
2427
+
2428
+ **排除的错误修法(实测)。**
2429
+
2430
+ - 「在 patch 里覆盖 `name` 为 `-browse`」不可行:`name` 是断言不是重命名(`dsh-app-boot/lib/index.js:546`),报 `name mismatch ... skipping`。
2431
+ - 「删掉 picker 替换,让 `-auto` 自选」不可取:macOS 本地宿主判定 native,`c2s.directory.list` 实测返回 `E_UNSUPPORTED: directory picker unavailable`,手机端目录浏览失效。
2432
+ - **验证方法约束**:`updateBundle` 的 pnpm install 会从 store 重新 clone 包,手改 `node_modules/dsh-deeppilot/cordis.patch.yml` 会被还原;改 bundle patch 必须发新包才能验证。运行时代码(已加载进内存)不受此影响,故 `src/plugin-update.ts` 的修复可在隔离副本产物上真机验证。
2433
+
2434
+ **本轮代码修复(B)。** `src/plugin-update.ts` 的 `restore` 分支改为以 `listBundles()` 实际安装版本为权威,`restore.application` 降为辅助判据;`tests/plugin-update.test.ts` 同步期望并新增「实际版本未回到旧版仍判 `failed`」。单测 21 通过 / 0 失败,全量 766 通过 / 0 失败,`typecheck` 通过。真机验证:同一宿主 journal 终态由 `failed` 变为 `rolled-back`(「目标安装失败,宿主已自动恢复旧版本」)。
2435
+
2436
+ **上游渠道。** `deepseek-ai/deepseek-harness` 的 `hasIssuesEnabled=false`、`hasDiscussionsEnabled=true`;README:45 指定 bug report 走 GitHub Discussions。已按用户确认的措辞发布 [discussion #12510](https://github.com/deepseek-ai/deepseek-harness/discussions/12510)(`q-a`/`Q&A` 分类,2026-11-07,作者 `Mars-Sea`);英文正文与中文核对说明留在 [UPSTREAM_DISCUSSION-plugin-update-hmr.md](UPSTREAM_DISCUSSION-plugin-update-hmr.md),脱敏后的提交正文在 `.verify-plugin-update/discussion-body.md`。线上标题、分类与正文长度已复核。等待上游答复,不承诺修复时限。
2437
+
2438
+ **遗留。** ① 仓库 `lib/` 未重建(按用户选择只在隔离副本构建),发布前需 `npm run build` 并提交 `lib/`;② 启用 HMR 的 profile 上手机端一键更新仍会失败,需上游修复 `updateBundle` 原子性,或用户在该 profile 的 `cordis.patch.yml` 禁用 `hmr` 行(代价:配置改动不再热重载、更新后需重启);③ 真机、Funnel、无 HMR 路径在官方默认 profile 上的验收仍缺;④ `docs/技能提示对接说明.md`、`docs/移动端命令对接说明.md` 的 `dsh-plugin-manager` 清单仍是 0.2.1 口径,未重盘。
@@ -45,7 +45,7 @@
45
45
  `package-lock.json` 顶层与根包条目的版本;提交并推送,等待持续集成(CI)通过。
46
46
  版本提交必须已经进入 `main`,且该提交包含 `publish.yml`。
47
47
  2. 经用户授权发布该版本后,从精确的发布提交创建并推送附注标签 `v<版本>`,
48
- 准备包含英文与简体中文说明的 GitHub 发布草稿。
48
+ 按 `AGENTS.md` 发布流程第 3 步的格式(中文在前、英文在后、短标题、不硬换行)准备 GitHub 发布草稿。
49
49
  3. 在对应标签上手动运行工作流。下例的 `0.9.5` 仅为示例,替换为已授权发布的版本:
50
50
 
51
51
  ```sh
@@ -211,6 +211,15 @@
211
211
  返回 200,`serverVersion` 为 `0.9.15`、`protocolVersion` 2、`dataPlane: true`,
212
212
  日志显示按需完成通知工具已注册;GitHub Release 在这一步通过后才公开并标记 Latest。
213
213
 
214
+ 14. `0.9.16` 的实际发布结果(2026-11-06):PR #43 压缩合并为 `1515b7b`,`main` 上 CI(`42170870772`)、
215
+ CodeQL(`42170870769`)、helper 构建(`42170870802`)均绿;附注标签 `v0.9.16` 指向
216
+ `1515b7b7059be850e6abf15071b1a5075e5bfd27`。`publish.yml` 运行 `42171736739`(`latest`)整次为绿,
217
+ **首次直接公开**,未进入 staged 区。注册表 `latest = 0.9.16`,来源证明类型为
218
+ `https://slsa.dev/provenance/v1`;69 个条目、解包 131,743,027 字节。随后在全新隔离 `DSH_HOME` 中从 npm
219
+ 安装 `dsh-deeppilot@0.9.16`,用真实 `0.2.2-rc.1` 宿主启动:`/phone/health` 返回 200,`serverVersion`
220
+ 为 `0.9.16`、`protocolVersion` 2、`dataPlane: true`,补丁未写 `local.enabled` 时局域网监听照常上线;
221
+ GitHub Release 在这一步通过后才公开并标记 Latest。
222
+
214
223
  ## 五、官方资料
215
224
 
216
225
  1. [npm 可信发布配置与限制](https://docs.npmjs.com/trusted-publishers/)
package/lib/index.d.ts CHANGED
@@ -1,5 +1,35 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
+ /**
4
+ * 输入框 `@` 引用候选(PROTOCOL.md「输入框引用扩展」)。
5
+ *
6
+ * 数据来源都是 DSH 官方 Remote,经 `typertGateway` 以 owner 身份调用,与 Web 端同一条发现链路:
7
+ * - 文件:`fileReferences/list {agentId, query}` → `[{path, kind}]`,由主机挂载的提供方
8
+ * (如 `dsh-file-reference-local`)在该会话工作目录里排序给出。只给路径,不读文件内容。
9
+ * - 会话:`sessionReferenceResolver/candidates {agentId, query}` → 带规范 mention
10
+ * `@[label](dsh-session:…)` 的候选;手机发送时原样放进提示词,主机负责改写与注入快照。
11
+ *
12
+ * 两类服务都是可选挂载(兼容下限 0.2.1-rc.5 没有保证),所以每次按当前服务是否存在判定,
13
+ * 一类缺席不影响另一类。会话候选不下发工作目录,只给 `sameWorkspace`,避免把本机路径带到手机。
14
+ */
15
+ interface FileReferenceItem {
16
+ path: string;
17
+ kind: 'file' | 'directory';
18
+ }
19
+ interface SessionReferenceItem {
20
+ sessionId: string;
21
+ label: string;
22
+ mention: string;
23
+ sameWorkspace: boolean;
24
+ }
25
+ interface ReferenceSource<T> {
26
+ available: boolean;
27
+ items: T[];
28
+ }
29
+ interface ReferenceCandidates {
30
+ files: ReferenceSource<FileReferenceItem>;
31
+ sessions: ReferenceSource<SessionReferenceItem>;
32
+ }
3
33
  declare const DEVICE_SCOPES: readonly ['sessions.read', 'prompt.send', 'sessions.manage', 'interactions.respond', 'notifications.register', 'schedule.manage'];
4
34
  type DeviceScope = (typeof DEVICE_SCOPES)[number];
5
35
  /**
@@ -352,6 +382,8 @@ interface HostSchedulePatch {
352
382
  interface WelcomeCapabilities {
353
383
  /** 会话命令目录、执行、权限与原生日志导出入口。缺省不支持。 */
354
384
  commands?: boolean;
385
+ /** 输入框 `@` 引用候选:c2s.session.references 可用(文件或会话引用至少一类)。缺省不支持。 */
386
+ references?: boolean;
355
387
  historyPaging: boolean;
356
388
  replay: boolean;
357
389
  approvals: boolean;
@@ -500,8 +532,8 @@ type MessageRole = 'user' | 'assistant' | 'tool' | 'system' | 'error';
500
532
  type ToolState = 'running' | 'ok' | 'error';
501
533
  /**
502
534
  * 文件编辑类工具调用的改动内容(v2 追加式扩展,见 PROTOCOL.md「工具改动对比」)。
503
- * Bridge 把宿主 `str_replace_editor` 的参数名(old_str/new_str/file_text/insert_line)
504
- * 归一化成这里的字段,手机端不依赖宿主工具参数命名。`oldText` / `newText` 各自超过
535
+ * Bridge 把宿主 `edit`(old_string/new_string)、`write`(content)与 `str_replace_editor`
536
+ * (old_str/new_str/file_text/insert_line)的参数名归一化成这里的字段,手机端不依赖宿主工具参数命名。`oldText` / `newText` 各自超过
505
537
  * 16 KiB(UTF-8)时都不发,只带 `truncated: true`。
506
538
  */
507
539
  interface ToolEditProjection {
@@ -513,6 +545,22 @@ interface ToolEditProjection {
513
545
  insertLine?: number;
514
546
  truncated?: boolean;
515
547
  }
548
+ /**
549
+ * 工具调用的操作对象(v2 追加式扩展,见 PROTOCOL.md「Message 投影 · tool.target」)。
550
+ * Bridge 从宿主工具参数里挑出「对什么做」:文件路径、命令、搜索式、网址或其他对象名,
551
+ * 参数名不外泄。工具结果合并回调用行后 `summary` 会被结果摘要替换,`target` 保留,
552
+ * 客户端据此在调用完成后仍能显示「读取 A.swift」而不是文件正文片段。
553
+ * `kind` 是开放词表:客户端遇到未知值按 `text` 处理。
554
+ */
555
+ interface ToolTargetProjection {
556
+ kind: 'path' | 'command' | 'pattern' | 'url' | 'text';
557
+ /** 主值;UTF-8 上限 1 KiB。路径超长时砍开头(保留文件名),其余砍结尾加「…」。 */
558
+ value: string;
559
+ /** 范围:搜索所在目录(`pattern` 专用);UTF-8 上限 1 KiB,规则同路径。 */
560
+ scope?: string;
561
+ /** 宿主给的人类可读说明(如 bash 的 description);UTF-8 上限 256 B。 */
562
+ label?: string;
563
+ }
516
564
  /** Provenance of host-injected context; present only on `role: "system"` rows.
517
565
  * Mirrors the durable DSH message source (`dsh-llm` MessageSource): `label`
518
566
  * names the producer (plugin name, skill name, instruction paths…), `form` is
@@ -547,6 +595,7 @@ interface MessageProjection {
547
595
  state: ToolState;
548
596
  summary: string;
549
597
  edit?: ToolEditProjection;
598
+ target?: ToolTargetProjection;
550
599
  };
551
600
  attachments?: MessageAttachment[];
552
601
  /** Present only on system rows: provenance of the injected context.
@@ -608,6 +657,16 @@ interface PendingSnapshotPayload {
608
657
  approvals: PendingApprovalPayload[];
609
658
  questions: PendingQuestionPayload[];
610
659
  }
660
+ /**
661
+ * 宿主持久化图片引用(ImageAttachmentRef)的原样对象。
662
+ *
663
+ * 只要求 `attachmentId`;其余字段(mediaType/bytes/width/height/name/
664
+ * originalDimensions…)由宿主生成、由宿主读取时自行核对,这里原样保留、不做
665
+ * 字段裁剪,免得宿主以后新增必填字段时被插件悄悄丢掉。
666
+ */
667
+ type HostImageRef = Readonly<Record<string, unknown> & {
668
+ attachmentId: string;
669
+ }>;
611
670
  interface CommandsApiLike {
612
671
  list(req: RpcRequestLike<{
613
672
  sessionId: string;
@@ -632,6 +691,13 @@ interface CommandsApiLike {
632
691
  format?: SessionExportFormat;
633
692
  }>, signal: AbortSignal): Promise<Response>;
634
693
  }
694
+ /** 输入框 `@` 引用候选(文件 + 会话),见 reference-api.ts。 */
695
+ interface ReferencesApiLike {
696
+ list(req: RpcRequestLike<{
697
+ sessionId: string;
698
+ query: string;
699
+ }>, signal?: AbortSignal): Promise<RpcResponseLike<ReferenceCandidates>>;
700
+ }
635
701
  interface RpcOk<T> {
636
702
  ok: true;
637
703
  value: T;
@@ -705,10 +771,16 @@ interface SessionsApiLike {
705
771
  cancel?(req: RpcRequestLike<{
706
772
  sessionId: string;
707
773
  }>): Promise<RpcResponseLike<Record<string, unknown> | undefined>>;
708
- /** Reads one durable image back after the host verifies the session log references its id. */
774
+ /**
775
+ * Reads one durable image back. DSH ≤ 0.2.1 reads `attachmentId` and checks the
776
+ * session log itself; DSH 0.2.2+ reads the full `attachment` reference and no
777
+ * longer checks the session, so HostBridge sends both and only supplies a
778
+ * reference it saw projected in that session.
779
+ */
709
780
  attachment?(req: RpcRequestLike<{
710
781
  sessionId: string;
711
782
  attachmentId: string;
783
+ attachment?: HostImageRef;
712
784
  }>): Promise<RpcResponseLike<{
713
785
  attachment: {
714
786
  mediaType?: string;
@@ -1097,6 +1169,8 @@ interface ApiProxyLike {
1097
1169
  /** 真实宿主反射声明了助手流帧时才为 true;旧代理缺省关闭。 */
1098
1170
  readonly assistantStreamAvailable?: boolean;
1099
1171
  commands?: CommandsApiLike;
1172
+ /** 主机挂载了文件引用或会话引用服务之一时存在;两者都缺席为 undefined。 */
1173
+ references?: ReferencesApiLike;
1100
1174
  sessions: SessionsApiLike;
1101
1175
  workspace?: WorkspaceApiLike;
1102
1176
  schedule?: ScheduleApiLike;
@@ -1277,6 +1351,8 @@ export declare class HostBridge {
1277
1351
  private abort;
1278
1352
  private started;
1279
1353
  private disposed;
1354
+ /** attachmentId → 宿主完整图片引用,供 attachmentData 补全 DSH 0.2.2+ 的读图参数。 */
1355
+ private readonly imageRefs;
1280
1356
  readonly promptDeliveries: DispatchJournal<{
1281
1357
  sessionId: string;
1282
1358
  content: unknown;
@@ -1417,12 +1493,28 @@ export declare class HostBridge {
1417
1493
  messages: MessageProjection[];
1418
1494
  hasMore: boolean;
1419
1495
  } | null>;
1420
- /** Result of one attachment read-back for the phone. */
1496
+ /**
1497
+ * Result of one attachment read-back for the phone.
1498
+ *
1499
+ * 宿主读图参数有两代:DSH ≤ 0.2.1 收 `{sessionId, attachmentId}` 并自己在会话日志里
1500
+ * 找引用;DSH 0.2.2 起改收 `{sessionId, attachment: 完整引用}`,且不再核对会话。
1501
+ * 控制器是直接注入的实例、不经参数 schema 校验,所以两代字段一起传:旧宿主读前者,
1502
+ * 新宿主读后者。完整引用来自 imageRefs(本会话投影过的图片);找不到时不带
1503
+ * `attachment`,旧宿主照常自查,新宿主读不到即回 null(E_NOT_FOUND)——手机因此
1504
+ * 读不到不属于该会话的图片。
1505
+ */
1421
1506
  attachmentData(sessionId: string, attachmentId: string): Promise<{
1422
1507
  mediaType?: string;
1423
1508
  data: string;
1424
1509
  } | null>;
1510
+ /** 图片引用回查用的原始历史页:只取事件,不投影、不推给手机。 */
1511
+ private readHistoryEvents;
1425
1512
  commandOperation(operation: 'list' | 'execute' | 'permissions' | 'feedback', payload: Record<string, unknown>): Promise<SessionManagementResult<unknown>>;
1513
+ /**
1514
+ * 输入框 `@` 引用候选。两类候选的单类失败已在 reference-api 内吸收为空列表,
1515
+ * 这里只处理「两类都不可用」与代理层意外异常。
1516
+ */
1517
+ referenceCandidates(sessionId: string, query: string, signal?: AbortSignal): Promise<SessionManagementResult<ReferenceCandidates>>;
1426
1518
  exportSession(sessionId: string, signal: AbortSignal, format?: SessionExportFormat): Promise<Response>;
1427
1519
  /** 由会话目录决定技能可见性;不读取正文,也不把本机路径发给移动端。 */
1428
1520
  sessionSkills(sessionId: string): Promise<SessionManagementResult<{