better-dsh 0.2.4-a → 0.2.4-c

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 (49) hide show
  1. package/cordis.patch.yml +7 -0
  2. package/docs/10_plans/2026-09-23-remote-execution-framework.md +236 -0
  3. package/docs/10_plans/2026-09-23-remote-framework-impl.md +2083 -0
  4. package/docs/10_plans/2026-09-26-lsp-ast-reminder.md +64 -0
  5. package/docs/20_specs/agent/spec.md +54 -0
  6. package/docs/20_specs/ast/spec.md +34 -0
  7. package/docs/20_specs/compaction-recall/spec.md +46 -0
  8. package/docs/20_specs/ctx/spec.md +107 -0
  9. package/docs/20_specs/dsh/spec.md +47 -0
  10. package/docs/20_specs/dvc/spec.md +87 -0
  11. package/docs/20_specs/escalation-guidance/spec.md +44 -0
  12. package/docs/20_specs/fs-scheme-resolution/spec.md +37 -0
  13. package/docs/20_specs/hash-edit/spec.md +41 -0
  14. package/docs/20_specs/http-read/spec.md +73 -0
  15. package/docs/20_specs/kernel-provisioning/spec.md +53 -0
  16. package/docs/20_specs/lsp/spec.md +121 -0
  17. package/docs/20_specs/mobile-layout/spec.md +108 -0
  18. package/docs/20_specs/model-failover/spec.md +20 -0
  19. package/docs/20_specs/plugins-page-components/spec.md +79 -0
  20. package/docs/20_specs/preact-ui-shell/spec.md +22 -0
  21. package/docs/20_specs/repl-dispatch-resilience/spec.md +21 -0
  22. package/docs/20_specs/skill/spec.md +58 -0
  23. package/docs/20_specs/tool-surface/spec.md +222 -0
  24. package/docs/20_specs/url-schema/spec.md +148 -0
  25. package/docs/20_specs/web-trust-fence/spec.md +43 -0
  26. package/docs/50_test-reports/2026-09-22-better-dsh/347/273/204/344/273/266/347/203/255/346/217/222/346/213/224/344/270/216/344/276/235/350/265/226/347/272/247/350/201/224/345/256/236/346/265/213/346/212/245/345/221/212.md +18 -1
  27. package/docs/50_test-reports/2026-09-23-remote-framework/345/256/236/346/265/213/346/212/245/345/221/212.md +92 -0
  28. package/docs/50_test-reports/2026-09-23-remote-framework/347/254/254/344/270/200/344/272/272/347/247/260/345/244/215/346/265/213/346/212/245/345/221/212.md +125 -0
  29. package/docs/50_test-reports/2026-09-23-ssh/345/267/245/345/205/267assisted-pty/344/274/232/350/257/235framing/345/256/236/346/265/213/346/212/245/345/221/212.md +207 -0
  30. package/docs/50_test-reports/2026-09-24-v0.1.7-rc.1/345/257/271/351/275/220/350/275/256-compaction/344/270/216failover/350/256/276/347/275/256/351/235/242/344/277/256/345/244/215/345/256/236/346/265/213/346/212/245/345/221/212.md +65 -0
  31. package/docs/50_test-reports/2026-09-27-lsp-ast-reminder-4999/345/256/236/346/265/213/346/212/245/345/221/212.md +53 -0
  32. package/lib/client/index.js +18 -18
  33. package/lib/compaction/index.d.ts +20 -10
  34. package/lib/compaction/index.js +42 -56
  35. package/lib/failover/index.d.ts +30 -6
  36. package/lib/failover/index.js +32 -26
  37. package/lib/fs-aware/sandbox-plugin.js +1 -1
  38. package/lib/index.js +4 -7
  39. package/lib/mobile/plugin.d.ts +1 -1
  40. package/lib/remote/plugin.d.ts +24 -0
  41. package/lib/remote/plugin.js +1095 -0
  42. package/lib/url-schemes/index.d.ts +14 -14
  43. package/lib/url-schemes/index.js +119 -15
  44. package/lib/{web-trust-CgpBoFv3.d.ts → web-trust-BihHCelF.d.ts} +10 -10
  45. package/lib/web-trust.d.ts +1 -1
  46. package/package.json +12 -4
  47. package/docs/10_plans/dashr-remote-tool-design.md +0 -337
  48. package/docs/10_plans/self-exo-cot-draft.md +0 -322
  49. package/docs/10_plans/self-scheme-and-monolog-draft.md +0 -357
@@ -0,0 +1,125 @@
1
+ # Remote 执行框架 — 第一人称复测报告(approval policy = `never`)
2
+
3
+ - **日期**: 2026-09-23(矩阵 20:52–20:59 +08:00)
4
+ - **分支/HEAD**: `remote-framework` @ `ac8720a`(**post-P15**:pty 4M 尾窗环 + deathWaiter 摘除)
5
+ - **复测载体**: **本 agent 自己的 live session**(Dev/Test 1 实例 4999 Web 运行时,`session-95c48291-f958-455c-83ab-98de3fa815a4`,cwd `/home/u1/workspaces/dashr`),直接以 `remote` 工具逐条派发——不是 headless 代跑
6
+ - **策略面**: 同一 session 内 `approval/policy: ask → never`(seq 2 → 6)、`sandbox/mode: workspace-write → danger-full-access`(seq 1 → 5);全部复测动作发生在 `never` 之后
7
+ - **对照**: 原验收报告 `docs/50_test-reports/2026-09-23-remote-framework实测报告.md`(矩阵 @ c696fca,**pre-P15**)+ SDD ledger `.superpowers/sdd/2026-09-23-remote-framework-impl/progress.md`
8
+ - **运行时同一性**: 实例 20:22:08 启动,`lib/remote/plugin.js` 构建于 20:21:34;rsync 后 worktree `src/` 与 monorepo 副本 `src/` `diff -rq` 为空 → **所测即 `ac8720a`**。P15 之前从未在真运行时跑过矩阵(Task 10 矩阵早于 `ac8720a`),本报告是 **P15 代码的首次第一人称实测**。
9
+
10
+ ---
11
+
12
+ ## 一、结论
13
+
14
+ | 面 | 结果 |
15
+ |---|---|
16
+ | 18 case 矩阵(本 agent 亲自派发) | **18/18 ✅**(1–8、10–18 直接复现;9 别名腿在 4999 原地热生效后复现) |
17
+ | P15/C1 输出帽(此前仅单测) | **✅ 第一人称**(oneshot 30k 帽 + pty 4M 环,尾保留,daemon 存活) |
18
+ | 原报告 §八 遗留「unreachable 真机路径」 | **✅ 补上真机证据**(`192.0.2.1` 黑洞 → `Connection timed out`) |
19
+ | 审计事件 | **✅** 本 session 日志含结构化 `dashr/remote-exec`(target/cmd/cwd/exit/durationMs,含 exit 130 中断) |
20
+ | approval 耦合 | **零**。`never` 下无一次 approval 询问/拒绝(63 次工具调用,0 错误,0 `approval/request|denied`) |
21
+ | `test/remote/` 单测 | **70/70 ✅**(9 spec) |
22
+ | 全量 vitest | 14 失败(**同基线 2 文件**)/ **578 通过** / 1 skip(593) |
23
+ | `tsc --noEmit` | **14 错,集合同基线,`remote` 0 提及** |
24
+
25
+ ---
26
+
27
+ ## 二、18 case 矩阵(本 session verbatim)
28
+
29
+ | # | case | 本 session 调用与结果 | 判定 |
30
+ |---|---|---|---|
31
+ | 1 | ssh oneshot | `{dev4, echo CASE1: … / git --version}` → `CASE1: 0 \| ok / git version 2.43.0`,exit 0 · 4.2s | ✅ |
32
+ | 2 | 退出码透传 | `{dev4, exit 42}` → exit **42** · 3.4s | ✅ |
33
+ | 3 | 复合语法 | `{dev4, ls / \| head -2 && echo $HOME}` → `bin / bin.usr-is-merged / /home/u1` | ✅ |
34
+ | 4 | pty 状态保持 | `cd /tmp && export F=1`(cwd `/tmp`)→ `pwd && echo $F` → `/tmp` `1` | ✅ |
35
+ | 5 | pty 多行单帧 | `"echo a\necho b"` → `a\nb` 一帧,exit 0 · 0.3s | ✅ |
36
+ | 6 | 伪 marker 免疫 | `printf '\033]133;D;deadbeefdeadbeef;99\007' ; echo real-after-marker` → `real-after-marker`,exit **0**(非 99) | ✅ |
37
+ | 7 | pty 超时打断 | `sleep 300` timeout 2 → `exit 130 · 2.2s · interrupted`;随后 `echo alive` → exit 0(会话存活) | ✅ |
38
+ | 8 | incus oneshot | `{incus:ctr-1, cat /etc/os-release \| head -1}` → `Ubuntu 26.04 LTS` | ✅ |
39
+ | 9 | incus pty + **别名路由** | 显式前缀 pty `cd / && hostname && pwd` → `ctr-1` `/`;**裸名 `ctr-1`** pty `cd / && echo via-alias && hostname && pwd && cat /etc/os-release \| head -1` → `via-alias / ctr-1 / / Ubuntu 26.04 LTS`,cwd `/` | ✅ |
40
+ | 10 | docker oneshot | 新起 `dashr-remote-t1`(ubuntu:24.04)→ `in-docker / Ubuntu 24.04.5 LTS` | ✅ |
41
+ | 11 | docker pty(script 托管) | `cd /root && pwd` → cwd `/root` · 0.1s | ✅ |
42
+ | 12 | BYO-PTY spawn | `{spawn:"docker exec -it dashr-remote-t1 bash", cmd:"echo spawned && hostname"}` → `spawned / de819743d988` | ✅ |
43
+ | 13 | 错误透明 | ssh:`ssh: Could not resolve hostname no-such-host-xyz: Temporary failure in name resolution`(exit 255);docker:`Error response from daemon: No such container: no-such-ctr`(exit 1)——原生 stderr 逐字 | ✅ |
44
+ | 14 | 断联自愈 | `kill -9 $$` → `exit null` · 0.2s;次调用 → 首行逐字 `[remote: session reconnected to fresh shell; cwd reset to default]` + `back /home/u1`,cwd `/home/u1` | ✅ |
45
+ | 15 | 后台输出 Non-Goal | `nohup sleep 5 >/dev/null 2>&1 & echo bg-ok` → 输出恰 `bg-ok`,stderr 空,**3.6s**(见 §四.1) | ✅ |
46
+ | 16 | status 可达探测 | 无 pty 的 `dev3`/`dev2`:`probe: reachable in 1438/1061ms (just now, on demand)` + `session: none — dials on first exec`;**全文无 "offline"** | ✅ |
47
+ | 17 | status 会话层 | 用过 pty 后 `{dev4}` → `session: idle 8s (connected)` | ✅ |
48
+ | 18 | status 容器事实/错误 | `{docker:no-such-ctr}` → `probe: error — error: no such object: no-such-ctr`;`{docker:dkr-1}`(已退容器)→ `probe: container exited` | ✅ |
49
+
50
+ **审计抽查 ✅**:本 session 日志(`session.v3.jsonl.zstd` 解压)含逐条 `dashr/remote-exec`:`{target, cmd, cwd, exit, durationMs}`——含 `sleep 300 → exit 130, 2233ms`、别名腿 `target: "ctr-1" → cwd "/", exit 0`、`target: "docker exec -it … bash"`(spawn)。工具面失败路径(Task 8 的 Ruling 14)本轮无异类失败可触发,未新增样本。
51
+
52
+ ---
53
+
54
+ ## 三、补测:原报告遗留缺口
55
+
56
+ ### 3.1 输出帽(P15 / 终审 C1,此前仅 5M 单测)
57
+
58
+ | 面 | 调用 | 结果 |
59
+ |---|---|---|
60
+ | oneshot 30k 帽 | `{dev4, python3 -c "print('x'*50000)"}` | 文本首行 `[truncated: showing last 30000 of 50001 chars]`,尾标 `truncated, original 50001 chars`,exit 0 · 3.4s |
61
+ | pty 4M 尾窗环 | `{dev4, mode:pty, head -c 5000000 /dev/zero \| tr '\0' 'y'; echo; echo TAIL-MARK-OK}` | 文本首行 `[truncated: showing last 30000 of 4000000 chars]`,**尾部 `TAIL-MARK-OK` 在位**,尾标 `truncated, original 4000000 chars`,exit 0 · 4.8s |
62
+
63
+ 判读:pty 收集段被 4,000,000 UTF-16 环截断(`OUTPUT_HARD_CAP`),模型面再经 30k 尾窗;超 5MB 输出下 daemon 无 OOM、无卡死、尾字节保留——**C1 修复在真运行时成立**。注意 `original 4000000` 是**环上界**而非真实总量(帽后切片,设计如此)。
64
+
65
+ ### 3.2 status 真机 `unreachable`(原报告 §八 缺口)
66
+
67
+ `{192.0.2.1}`(TEST-NET 黑洞)→
68
+
69
+ ```
70
+ 192.0.2.1 — ssh host
71
+ probe: unreachable — ssh: connect to host 192.0.2.1 port 22: Connection timed out
72
+ session: none — dials on first exec
73
+ ```
74
+
75
+ 词汇表 `unreachable — <native error>` 自此有真机证据(此前仅单测标签路径)。
76
+
77
+ ### 3.3 别名路由的**热生效**事实(新发现)
78
+
79
+ 原 Task 10 把 case 9 的别名腿当"改 home 层 patch(隐含需重启)"处理。本次实测:写入 `.dsh-test/cordis.patch.yml`(`[]` → 一行 `dashr-remote` 行重述 + `containers: {ctr-1: incus:ctr-1}`),**约 3s 内 4999 活体自行重新对账**——
80
+
81
+ - 之前:`{ctr-1}` → `ctr-1 — ssh host`(~/.ssh/config 同名 Host 胜出)
82
+ - 之后:`{ctr-1}` → `ctr-1 — incus container` + `probe: container RUNNING`,pty 落容器(`via-alias / ctr-1`)
83
+ - 复原(`[]`)同样 ~3s 热生效:`{ctr-1}` 回到 `ssh host`
84
+
85
+ 即 0.1.6 的 HMR reconcile 覆盖 home 层 patch 写读,**别名腿无需重启**。副产品观察见 §四.2。
86
+
87
+ ---
88
+
89
+ ## 四、与原报告的差异 / 真实观察(均不阻塞)
90
+
91
+ 1. **case 15 的"oneshot 等 ssh 会话通道 ~16.6s"未复现**。本轮同形命令 **3.6s** 返回(≈常规 ssh 握手成本),输出/退出码/静默性均符 Non-Goal。原 §六 第 1 条的持留观察在本环境本轮不成立;描述文本是否补披露可再议(不据此改代码——一次观测 vs 一次观测,证据对等)。
92
+ 2. **配置热重载会丢弃 live pty 会话**:别名行热生效后,此前 idle 的 dev4 pty 会话从 `session: idle 8s (connected)` 变为 `session: none — dials on first exec`(插件行重挂 → `PtyPool` 重建)。行为可解释(重挂即新建 driver),但**不是** `idleTtlSec` 的语义,属"配置热改的代价",记录在案。
93
+ 3. **case 17 的 idle 语义**在热重载前验证成立(8s connected);热重载后必然归 none,两者不矛盾。
94
+ 4. dev4 无 docker,嵌套穿透(ssh -t 内嵌 docker)仍未覆盖(与原报告一致)——spawn 腿(case 12)已独立证 BYO-PTY 机制。
95
+
96
+ ---
97
+
98
+ ## 五、approval policy `never` 的专门核对
99
+
100
+ - 同一 session 策略事件:`approval/policy {ask}`(seq 2, 20:46)→ `{never}`(seq 6, 20:50);`sandbox/mode {workspace-write}` → `{danger-full-access}`。
101
+ - `never` 生效后:**63 次工具调用、0 次 `approval/request` / `approval/denied` / `sandbox/denied`**,18 case + 补测全部完成。
102
+ - 源码面核对:`src/remote/*` 无 `approval` / `sandbox` / `escalat` 任何引用(grep 0 命中)——本框架在 daemon 侧直接派发系统 CLI,**不经** agent 工具审批链,因此策略从 `ask` 收紧到 `never` 对 `remote` 功能面无影响。这条正是本轮复测要回答的问题。
103
+
104
+ ---
105
+
106
+ ## 六、单测 / 类型基线(worktree @ `ac8720a`)
107
+
108
+ - `npx vitest run test/remote/` → **Test Files 9 passed (9),Tests 70 passed (70)**(nonce 13 / target 5 / transports 9 / oneshot 6 / pty-session 14 / driver 6 / status 7 / tool 7 / plugin 3——pty-session 较原报告的 13 多 1,即 P15 的 5M 回归)。
109
+ - `npm test` → **14 failed | 578 passed | 1 skipped (593)**;失败恒为 **2 文件**:`test/url-schemes.spec.ts`(2)+ `test/surface-devices/agent-family.spec.ts`(12),单独复跑同得 `14 failed | 41 passed`——**全为 main 既有病,与本特性无关**。(ledger 记的 577 为 `ac8720a` 前数值;P15 新增 1 测试后应为 578。)
110
+ - `npx tsc --noEmit` → 14 错,文件集合同基线(`src/index.ts` 3、`src/url-schemes/index.ts` 1、`src/url-schemes/handlers/agent.ts` 1、`test/url-schemes.spec.ts` 2、`test/url-schemes/gates.spec.ts` 1、`test/surface-devices/agent-family.spec.ts` 6);**`remote` 0 提及**。
111
+
112
+ ---
113
+
114
+ ## 七、清理与现场状态
115
+
116
+ - 别名 patch:`.dsh-test/cordis.patch.yml` 已还原为 `[]`(原文件备份 `.scratch/cordis.patch.yml.bak-task10retest`),并已复核热还原。
117
+ - scratch 容器 `dashr-remote-t1` 已 `docker rm -f`;`docker ps -a` 无残留(`{docker:dashr-remote-t1}` 现报 `no such object` 即为清理后真值)。
118
+ - 未触碰 `corti` / `jellyfin` / `ctr-1` / `app` 等既有资产;未改任何源码或构建产物;未重启 4999(本轮全程在同一 session 内完成)。
119
+ - 证据落盘:解压后的本 session 日志 `.scratch/retest-session.jsonl`(356 行,含全部 audit 事件)。
120
+
121
+ ## 八、遗留
122
+
123
+ - 复核结论:**矩阵与缺口补测全过,approval `never` 无影响;分支 `ac8720a` 的实测证据面现覆盖 post-P15 代码**。
124
+ - 原报告 §八 其余两项仍开放(仅作里程碑候选):`ws://` transport(spec 远期);oneshot 会话通道持留语义的披露措辞(且本轮未复现,证据对等)。
125
+ - 本轮产物:本报告;无源码/测试改动,故无新单测需求。
@@ -0,0 +1,207 @@
1
+ # ssh 工具 assisted-pty 远程会话(framing / 边界)实测报告
2
+
3
+ - 日期:2026-09-23
4
+ - 环境:workspace host(本 GUI 会话)→ 目标 `dev4`(`u1@137.131.54.174`,key `~/.ssh/id_key4`)与 `ctr-1`(本机 **Incus 容器**,`u1@10.245.164.100`,key `~/.ssh/id_key1`)
5
+ - 触发:user「try ssh dev4」+「try ctr-1, lxd, any diff」;本报告为**探索性实测**,非发版验收
6
+ - 结论摘要:会话机制解剖完成(**pty + 生成式 rcfile + OSC 133;D / OSC 7 framing**);发现 4 类值得处理的行为——**①OSC 133;D 注入可伪造 exit code 并吞掉 stdout**、**②`timeout` 仅以 SIGINT 尽力中断**、**③后台写者可污染下一条命令的 stdout**、**④同 target 命令串行化(长命令阻塞队列)**;其余为设计内行为。**dev4(VM)与 ctr-1(容器)的会话语义逐项一致**,差异只在环境本身(bash/locale/TZ/进程名)。
7
+
8
+ ---
9
+
10
+ ## 一、被测对象
11
+
12
+ | target | 形态 | 连接 | 备注 |
13
+ |---|---|---|---|
14
+ | `dev4` | 远程 VM | `137.131.54.174:22`,`~/.ssh/id_key4` | rtt 0.7–1.2 s;`bash 5.2.21`,`LANG=C.UTF-8`,TZ `+08:00` |
15
+ | `ctr-1` | **本机 Incus 容器**(`incus list`:`ctr-1, CONTAINER`) | `10.245.164.100:22`,`~/.ssh/id_key1` | rtt ~0.5 s;`bash 5.3.9`,`LANG=en_US.UTF-8`,TZ `+00:00`;`systemd-detect-virt` → `lxc` |
16
+ | `lxd` | — | — | **不存在**:`[E_UNKNOWN_TARGET]`;已知 target = `github.com, dev2, dev3, mac, dev1, 137.131.54.174, dev4, ctr-1` |
17
+
18
+ 状态变更(已记录,便于回滚):**`ctr-1` 实测前为 `STOPPED`,本次由 `incus start ctr-1` 拉起,报告时点仍为 RUNNING**(`incus stop ctr-1` 可还原)。
19
+
20
+ 附带(与 ssh 无关,user 指令):删除 `dev4:~/openclaw-20260612-010937.tar.gz`(615,409,809 B,home 下唯一 openclaw 文件、无进程持有;`rm -v` 后复查已不存在)。
21
+
22
+ ---
23
+
24
+ ## 二、会话机制解剖(关键,后文所有行为都由此解释)
25
+
26
+ 远端进程树(`ps -o pid,ppid,cmd -p $$ -p $PPID`,dev4):
27
+
28
+ ```
29
+ PID PPID CMD
30
+ 1169518 1169458 sshd: u1@pts/1
31
+ 1169728 1169518 bash --rcfile /tmp/.better-dsh-rc-6e1e345f3a3e4493
32
+ ```
33
+
34
+ rcfile 全文(`/tmp/.better-dsh-rc-<hash>`,两个 target 上内容一致):
35
+
36
+ ```bash
37
+ # dashr engine rcfile — generated by dashr/remote/framing, do not edit.
38
+ for f in ~/.profile ~/.bash_profile ~/.bashrc; do [ -f "$f" ] && . "$f"; done
39
+ stty -echo 2>/dev/null || true
40
+ PS1=''
41
+ PS2=''
42
+ PS0=$'\033]133;C\007'
43
+ PROMPT_COMMAND='printf "\033]133;D;%s\007" "$?" ; printf "\033]7;file://%s\007" "$PWD"'
44
+ export TERM=dumb
45
+ ```
46
+
47
+ 由此可判定的机制事实:
48
+
49
+ 1. **传输是真正的 pty**(`tty` → `/dev/pts/N`,`test -t 0/1/2` 全 yes;`stty -a`:38400 baud、40×120、canonical、`intr=^C`、`erase=^?`、`eof=^D`),不是 `bash -lc` 无 tty 管道;
50
+ 2. 会话是**单条常驻交互 bash**(跨调用 PID 不变),命令由 pty 送入、以 `PS0` 的 **OSC 133;C** 起帧、以 `PROMPT_COMMAND` 的 **OSC 133;D;<exit>** 收帧;**exit code 来自终端字节流解析**,不是 ssh 的 channel exit-status;
51
+ 3. **cwd 来自 OSC 7**(`file://$PWD`),`ssh({target})` 状态行的 `cwd:` 即此;
52
+ 4. `stty -echo` 关回显(所以 `stdin` 参数喂入的内容不会在输出里出现);`PS1=''` 保证无提示符噪声;`TERM=dumb` 抑制分页/颜色协商。
53
+
54
+ ⚠️ 状态行自称 `env: login-shell (bash -lc)` 与实测不符——实际是**交互式 bash + 生成式 rcfile**(rcfile 内手动 source 了 `~/.profile ~/.bash_profile ~/.bashrc` 来模拟 login 语义)。属文案偏差,非功能缺陷。
55
+
56
+ ---
57
+
58
+ ## 三、缺陷 / 风险(按严重度)
59
+
60
+ ### 3.1 🔴 OSC 133;D 注入:远端输出可伪造 exit code 并吞掉 stdout
61
+
62
+ 命令自身只要往 stdout 写 `ESC ] 133 ; D ; <n> BEL`,就会被 framing 当作**本命令的结束标记**:
63
+
64
+ | 命令 | 观测结果 |
65
+ |---|---|
66
+ | `printf '\033]133;D;99\007'` | `[exit 99]`,stdout 为空(后续 `echo after-osc133-spoof` 被吞) |
67
+ | `echo before-marker; printf '\033]133;D;55\007'; echo after-marker; false` | 输出仅 `before-marker`,`[exit 55]`(**真实 exit 为 1,被覆盖**) |
68
+ | `printf 'MID\033]133;D;66\007TAIL\n'; echo end` | 输出仅 `MID`,`[exit 66]`(**行中位置同样生效**) |
69
+
70
+ 影响:任何远端程序/日志/被下载内容里的这一个字节序列,都能让 agent 得到**假失败或假成功**,并让 stdout 在注入点被截断(静默丢证据)。dev4 与 ctr-1 均复现(ctr-1 实测 `[exit 88]`)。这是本报告唯一建议**优先修**的项。
71
+
72
+ 对照:**OSC 7 注入不生效**(两条独立实证,见 §四.3)——即同一套 framing 对两类 OSC 的信任度不一致,说明「只认自己发的 marker」这件事是**可做到的**,133;D 侧漏了。
73
+
74
+ ### 3.2 🔴 `timeout` 是 SIGINT 尽力中断,不是 kill
75
+
76
+ - 超时路径经 pty 发 `^C`:`cat`(stdin 阻塞)`timeout: 3` → `[exit 130 (=128+SIGINT) · interrupted]`,3.2 s 返回,符合预期;
77
+ - 但**忽略 SIGINT 的命令能整段跑完**:`bash -c 'trap "" INT; echo ctr-trapped-int; sleep 4; echo ctr-survived-SIGINT'`,`timeout: 2` → 实跑 **4.0 s**,输出两行齐全,`[exit 0 · interrupted]`。dev4 侧同测(`sleep 5` / `timeout: 3` → 5.2 s)同样复现。
78
+
79
+ 影响:挂死且不吃 SIGINT 的命令(编辑器、REPL、`trap '' INT` 脚本)**不会被 timeout 终止**,且最终以 exit 0 上报,agent 会误判为成功。
80
+
81
+ ### 3.3 🟠 跨命令 stdout 污染:后台写者会混进下一条命令的输出
82
+
83
+ framing 会丢弃**命令之间**的空档输出,但**下一条命令执行期间**到达的输出会被算进该命令:
84
+
85
+ - ctr-1:调用 A `( for i in 1 2; do sleep 2; echo CTR-LEAK-$i; done ) & echo bg-launched`(瞬时返回);调用 B `bash -c 'trap "" INT; … sleep 4 …'` 的输出里出现 `CTR-LEAK-1`、`CTR-LEAK-2` 及作业控制行 `[1]+ Done …`;
86
+ - dev4 复现:调用 A 起 `for i in 1 2 3 … echo D4-LEAK-$i`(间隔 1 s),调用 B `echo start-slow-dev4; sleep 4; echo end-slow-dev4` → `D4-LEAK-1..3` 夹在 `start-slow-dev4` / `end-slow-dev4` 之间。
87
+
88
+ ⚠️ 修正一个早期结论:同测法在 dev4 上曾「看不到泄漏」,原因是**后台任务已跑完、输出落在两条命令的空档被丢弃**(时序巧合),并非免疫。凡「上一条命令留下存活的后台写者 + 下一条命令耗时」即污染可复现。
89
+
90
+ ### 3.4 🟡 同 target 命令串行化(正确但会阻塞)
91
+
92
+ 同一 target 的并发调用被排队(实测:`sleep 4` 调用 + 立即 `echo` 调用 → 后者 0.2 s 返回,用时互不重叠)。判别实验:调用 A 阻塞在 `read`(`timeout: 8` → 8.3 s `interrupted`),同批调用 B `echo B-INPUT-MARKER` **未被当作 A 的 stdin**、正常返回 —— 说明是**队列**而非并发共享 pty。
93
+
94
+ 影响:一条挂死命令会阻塞该 target 的后续所有调用(含 `ssh({target})` 状态查询);跨 target 并行不受影响。**不要对同一 host 并发 fan-out**,要并行就换 target 或写成一条远端脚本。
95
+
96
+ ---
97
+
98
+ ## 四、设计内行为(记录,供后续依赖/文档化)
99
+
100
+ 1. **完整 shell 状态跨调用持久**(同一 bash PID):cwd、导出变量与非导出变量、shell 函数、alias、shell 选项(`set -o pipefail` 实测跨调用为 on)、作业控制均保留 → **一次 `set -e` / `cd` / `unset` / alias 会静默影响后续所有调用**;用完须显式还原。
101
+ 2. **命令为简单复合时进程即会话 shell 本身**(`$$ == $BASHPID`,PPID 为 sshd)——所以「子 shell 里的 cd」不生效、而顶层 `cd` 生效。
102
+ 3. **`exit` / `kill -9 $$` 杀掉会话 shell** → 工具上报 `[exit null]`(无 exit code),随后**新建 bash**(rcfile hash 变化,dev4 累计 3 个 rcfile;ctr-1 复用同一 hash,仅 1 个——差异未解释,仅记录);导出变量丢失;**cwd 恢复到最后一次「成功完成命令」上报的路径**:
103
+ - 实证(ctr-1):`cd /etc` 成功上报 → 下一条命令 `cd /var/tmp && kill -9 $$`(未上报即死)→ 恢复后 `pwd` = **`/etc`**(非 `/var/tmp`)。
104
+ - OSC 7 伪造不改变该恢复:`printf '\033]7;file://ctr-1/usr\007'; kill -9 $$` → 恢复仍 `/etc` 而非 `/usr`;另测伪造 `/etc` 后状态行仍显示真实 `/tmp`。**对 OSC 7 的信任是收敛的**。
105
+ 4. **输出截断为尾偏**:单次 338,959 chars → 只回**最后 30,000 chars**,头部丢弃,首尾均有 `truncated` 标记。(代价:把状态回显放在长输出开头会被截掉;诊断请用 `wc`/`tail`。)
106
+ 5. **stdout/stderr 合并**为同一有序流(stderr-only 正常出现);**ANSI 转义原样保留**(`1b 5b 33 31 6d…`);**NUL 字节被吞成空格**(`printf 'x\0y'` → `x y`)→ 二进制请 base64。
107
+ 6. **stdin 可用且是 pty**:`stdin` 参数对 `read` 生效(`read -r first; read -r second` 拿到两行);裸 `cat` 无输入则永久阻塞(→ 超时 130)。
108
+ 7. **后台进程存活,不阻塞返回**:`setsid` / `nohup` / 裸 `&` 三者在会话 shell 被 kill 后均存活并完成写文件;`nohup … &` 会在 home 造 `~/nohup.out`(实测后已清理)。
109
+ 8. **错误面**:未知 target → `[E_UNKNOWN_TARGET]` 并列出已知 target;连接不可达 → `error : ssh connect failed: connect EHOSTUNREACH …`(ctr-1 停机时实测)。
110
+ 9. 其它:`sudo -n whoami` → `root`(dev4/ctr-1 均 NOPASSWD);emoji/CJK/heredoc round-trip 正常;`locale` 见 §五。
111
+
112
+ > 探针自身的一个坑(非工具缺陷):在 `$( )` 里跑 `test -t 1` 会因命令替换把 fd1 接成管道而返回 false,最初误判「stdout 不是 tty」;直接在顶层测为 yes。
113
+
114
+ ---
115
+
116
+ ## 五、dev4(VM) vs ctr-1(容器)差异表
117
+
118
+ | 维度 | dev4 | ctr-1 | 是否影响会话语义 |
119
+ |---|---|---|---|
120
+ | 会话机制 | pty + `bash --rcfile /tmp/.better-dsh-rc-<hash>` | 同 | **无差异** |
121
+ | rcfile 内容 | 8 行,同款 | 同款(逐字节一致) | 无 |
122
+ | `bash` | 5.2.21(1) | 5.3.9(1) | 无(framing 只依赖 PS0/PROMPT_COMMAND/OSC) |
123
+ | locale / TZ | `C.UTF-8` / `+08:00` | `en_US.UTF-8` / **`+00:00`** | 无(但注意 date 差 8 小时) |
124
+ | sshd 进程名 | `sshd: u1@pts/1` | `sshd-session: u1@pts/1` | 无 |
125
+ | 会话 PID 量级 | 116xxxx | 3xx(容器 PID ns) | 无 |
126
+ | `busybox` | 有(`/usr/bin/busybox`) | 无 | 无 |
127
+ | 虚拟化标识 | — | `systemd-detect-virt=lxc`,内核 `7.0.0-30-generic`(宿主内核) | 无 |
128
+ | sudo | NOPASSWD | NOPASSWD | 无 |
129
+ | 行为项(exit code / OSC spoof / SIGINT / 持久化 / kill 恢复 / 截断) | 一致 | 一致 | — |
130
+
131
+ 结论:**framing 层对「VM / 容器」无感**;差异全部来自 guest 环境本身。
132
+
133
+ ---
134
+
135
+ ## 六、非 SSH 传输对照(同机 `ctr-1` 与 `corti` 容器)
136
+
137
+ | 传输 | 默认 pty | `TERM` | 观测 |
138
+ |---|---|---|---|
139
+ | `ssh`(本工具) | ✅ 总是 | `dumb`(rcfile 强制) | `tty=/dev/pts/N`,40×120 |
140
+ | `incus exec ctr-1 -- bash -lc …` | ❌ | 继承 `dumb` | `tty` → `not a tty` |
141
+ | `incus exec -t ctr-1 -- …` | ✅(显式 `-t`) | `dumb` | `tty=/dev/pts/2` |
142
+ | `docker exec corti sh -c …` | ❌ | **未设置** | `not a tty`;`TERM=<unset>` |
143
+
144
+ 即:只有 ssh 面默认给 pty;`exec` 类传输需要显式 `-t`,且 `TERM` 需自行注入——这正是下面附录 B 讨论的接口面。
145
+
146
+ ---
147
+
148
+ ## 七、复现命令清单(全部为本报告实测命令)
149
+
150
+ ```bash
151
+ # 机制解剖
152
+ ps -o pid,ppid,cmd -p $$ -p $PPID; cat /tmp/.better-dsh-rc-*; stty -a
153
+
154
+ # 3.1 OSC 133;D 注入
155
+ printf '\033]133;D;99\007'
156
+ echo before-marker; printf '\033]133;D;55\007'; echo after-marker; false
157
+
158
+ # 3.2 timeout 只发 SIGINT
159
+ bash -c 'trap "" INT; echo trapped-int; sleep 5; echo survived' # ssh timeout: 3 → 5.2s / exit 0
160
+
161
+ # 3.3 跨命令 stdout 污染
162
+ ( for i in 1 2 3; do sleep 1; echo LEAK-$i; done ) & echo launched # 调用A
163
+ echo start; sleep 4; echo end # 调用B(LEAK-n 混入)
164
+
165
+ # 3.4 串行化判别
166
+ echo waiting; read -r x; echo "got:[$x]" # 调用A(timeout: 8,阻塞)
167
+ echo B-INPUT-MARKER # 调用B(未被当作 A 的 stdin)
168
+
169
+ # 四.3 cwd 恢复语义
170
+ cd /etc; echo reported-from-etc # 调用A
171
+ cd /var/tmp && kill -9 $$ # 调用B → exit null
172
+ pwd # 调用C → /etc(非 /var/tmp)
173
+ printf '\033]7;file://ctr-1/usr\007'; kill -9 $$ # 伪造后再杀 → 仍 /etc
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 八、建议(未实施,待裁决)
179
+
180
+ 1. **133;D 收帧加固**:只接受「帧内首个、且位于命令输出之后的 harness 自产 marker」不可靠;建议给 marker 加**每会话随机 nonce**(`133;D;<nonce>;<exit>`)或改用独立边带通道(stdout 之外的 fd / 文件),使远端输出无法自证结束。若不愿改协议,退而求其次:解析时**取最后一个 133;D**(`PROMPT_COMMAND` 必然在命令之后执行)可让「命令内注入」失效——但命令若在注入后自杀(`exit`)仍可绕过,故 nonce 才是根治。
181
+ 2. **timeout 二段式**:SIGINT 后 N 秒未收帧即升级 SIGKILL(需 kill 远端进程组,pty 前台 pgid 可查)。
182
+ 3. **污染面**:收帧后到下一 `133;C` 之间的输出应显式丢弃并计数;或在 3.3 场景下把 job-control 噪声也过滤掉。
183
+ 4. **文案**:状态行 `login-shell (bash -lc)` → 改为 `assisted pty (bash --rcfile)` 之类,避免误导。
184
+ 5. **文档化**(面向使用方):状态跨调用持久、同 target 串行、尾偏截断、忽略 SIGINT 的命令不会被超时终止——这 4 条会直接改变 agent 的使用策略。
185
+
186
+ ---
187
+
188
+ ## 附录 A:非常规测点清单(本次覆盖)
189
+
190
+ pty/tty 三 fd 语义 · `stty -a` 行规程 · 回显/`stty -echo` · OSC 133;C/133;D/7 语义与伪造 · exit code 通道(子 shell/管道 `PIPESTATUS`/`command not found` 127/SIGTERM 143/SIGKILL 137/`exit` → null)· `set -e`/`pipefail` 跨调用残留 · alias/函数/非导出变量持久 · cwd 持久与 kill 后恢复 · stdin(`read`/裸 `cat`/`stdin` 参数)· 大数据截断方向 · NUL/CR/ANSI 字节保真 · stdout+stderr 合并序 · heredoc/引号/emoji/CJK · 后台进程存活(setsid/nohup/`&`)与 `~/nohup.out` · 后台输出跨命令污染 · 并发同 target 串行 vs stdin 误食 · 未知 target 与不可达 target 报错面 · sudo NOPASSWD · 容器 vs VM 差异 · `incus exec`/`docker exec` 无 pty 对照。
191
+
192
+ ## 附录 B:通用化讨论(**非实测**,讨论稿,2026-09-23)
193
+
194
+ > user 议题:「如果所有 remote 都提供这种 assisted pty session——docker、远程机器 over websocket」。以下为基于本次实测的推演,未落地。
195
+
196
+ **收益(为什么值得统一)**:assisted pty 把「远程执行」从『一次性 `exec` 取 stdout』升级成**有状态的会话面**——持久 cwd/env、可交互 stdin、真实 tty(TUI/彩色/进度条/`sudo` 提示都可工作)、以及通过 framing 拿到的**结构化 exit code + cwd**。同一套 framing 换个 transport 即可复用,agent 侧只认一套语义(本次 dev4/ctr-1 逐项一致就是证据:framing 对 VM/容器无感)。
197
+
198
+ **成本/风险(本次已暴露的都在这里放大)**:
199
+ - **信任边界翻转**:marker 走的是**被观测对象的输出**。133;D 注入已经能伪造 exit code;一旦 transport 变成 docker daemon / WebSocket 网关这种「中间层」,多一个可注入 stdout 的位置就多一个伪造点。正解是 **nonce / 边带通道**,让「结束」由 harness 自己证明,而不是由远端输出自证。
200
+ - **生命周期归属**:pty 会话是长命进程。docker 容器重建、WebSocket 断线重连、k8s pod 漂移之后,**会话必须被判死并重建**,否则会出现「协议还对、shell 已换人」的僵尸会话(本次 `kill -9` 后 PID 变更 + rcfile hash 变更即其简化版)。需要会话身份(boot id/nonce)随帧下发,客户端校验不一致即重连。
201
+ - **状态泄漏**:持久 shell 是把双刃剑(§四.1)。容器场景下更危险——一个 agent 的 `export TOKEN=`/`cd`/alias 会跨 turn、甚至跨「逻辑任务」残留;多云多租户时这就是横向面。建议每条 framing 命令都带**可选无状态模式**(等价 `bash -lc` 语义:新 shell、显式 cwd、显式 env),把「持久」变成 opt-in 而非默认。
202
+ - **背压与串行化**:本次同 target 串行是安全选择,但 `docker exec`/WebSocket 是**天然可并发**的 channel;若照抄「一会话一队列」,长任务会互相阻塞。需要区分「会话(pty,串行)」与「命令(channel,可并发)」两层,并把强制串行限制在真的共享 pty 的那一层。
203
+ - **`TERM` 与噪声**:`incus exec`/`docker exec` 默认无 tty 且 `TERM` 缺失/继承(§六)。统一层必须**显式分配 pty 并注入 TERM**,否则 TUI 类工具在容器里行为漂移;同时 `PS1=''`+`stty -echo` 这类「消噪」也要成为 transport 契约的一部分,而不是 ssh 专属技巧。
204
+ - **窗口/尺寸与控制字符**:40×120 是本次观测到的固定尺寸。docker/WebSocket 侧需要 `TIOCSWINSZ` 协商,否则 `less`/`top`/`gdb` 一类程序布局错乱;`^C`/`^D` 语义(本次 timeout=SIGINT)也必须定义成契约,而不是各 transport 自行其是。
205
+ - **可观测性**:既然 exit code 与 cwd 都来自字节流,那么**字节流本身就是审计证据**。建议 transport 层留存原始帧(含 marker 与注入点),这样 133;D 这类事件可被事后检出,而不是静默生效。
206
+
207
+ **一句话**:assisted pty 的**语义**(有状态、可交互、结构化收帧)值得在 docker / WebSocket 上复用,但**信任模型**必须同时升级——marker 必须由 harness 用 nonce/边带自证,会话身份必须可校验,状态持久与并发必须显式可选。否则就是把 ssh 面已经暴露的三个坑(自证结束、僵尸会话、状态残留)复制到更多 transport 上。
@@ -0,0 +1,65 @@
1
+ # v0.1.7-rc.1 对齐轮 — compaction / failover 设置面修复实测报告(2026-09-24)
2
+
3
+ 分支 `align/v0.1.7-rc.1`(基线 main = 0.2.4-b,prod 同版);提交 `5aa9ca0` + `eea888f`。
4
+
5
+ ## 一、症状与根因
6
+
7
+ prod(3080)已运行 dsh **0.1.7-rc.1**(npm,`~/.local`)+ better-dsh **0.2.4-b**。用户报告:
8
+
9
+ 1. **compaction 组件不在 Settings/General 出现**;
10
+ 2. **failover 设置行在,但选择后报"cannot save to file"**。
11
+
12
+ 根因只有一个 —— **dsh 0.1.7 移除了 settings.yaml namespace 模型**:
13
+
14
+ | 0.1.6 及以前(better-dsh 0.2.4 所说方言) | 0.1.7-rc.1(现实) |
15
+ |---|---|
16
+ | `SettingsProvider.installSection(ctx, ns, schema, base, hooks)` | **已删除**(新 `SettingsForms` 无此方法) |
17
+ | namespace = 自由字符串,存 `settings.yaml` | namespace = **Loader entry id**;值以 profile patch 用户层落盘(configEditor) |
18
+ | `settings.get(ns)` | 已删除 |
19
+ | 自定义写校验钩子(validate) | 无此 seam(只剩 schema 校验) |
20
+ | v3 消息 source `{kind:'plugin', plugin}` | v4 producer-owned kind(`plugin:better-dsh`,migration fallback 即此形) |
21
+
22
+ 两个症状的精确链条:
23
+
24
+ - **compaction 行消失**:`CompactionRow.loadConfig()` 在 `describe()` 里找不到 `ns === 'compaction-tuning'`(0.1.7 的 ns 是 entry id `dashr-compaction-tuning`)→ 返回 null → `CompactionRow` 对 absent namespace **渲染 null**。行没注册失败,是自己选择不渲染。
25
+ - **failover 存不进**:行本身渲染不依赖 namespace(空默认也渲染),但 save 走 `remote.settings.update('failover', …)` → 0.1.7 `SettingsForms.write()` 抛 **`No configurable plugin entry "failover"`** → 行内 toast `Save failed: settings/rejected: …`(用户转述为"cannot save to file";该字符串不属上游也不属 better-dsh,是 toast 模板 + 服务错误的拼接)。
26
+ - **隐藏第三伤(用户未及看到)**:两个 host 半的 `ctx.inject(['settings'], sctx => sctx.settings.installSection(…))` 在 0.1.7 会 TypeError —— failover 的 per-turn 切换链、compaction 的引擎同步实际都已残废。
27
+
28
+ ## 二、修复(对齐 upstream `agent-default-model` 范式)
29
+
30
+ `packages/core/agent-default-model/src/index.ts` 是 0.1.7 的官方样例:`.volatile()` 字段以 `Volatile<T>` live ref 进入 resolved config(runtime 原地改写 ref,读侧 `.get()` 即实时)、`settings.configure({auto:false}, fiber)` 压掉原生自动表单、持久化交给可选 `configEditor.edit(entry,…)`。better-dsh 逐条对齐:
31
+
32
+ - **schema**:`fallback1/2`、`thresholdRatio` 加 `.volatile()`(`retainTokens` 保持非 volatile = 组合面专用,表单写永不触碰)。
33
+ - **failover host**:删 installSection;`source` 每次请求/失败实时 `readSlot(ref)`;`configure({auto:false})`。
34
+ - **compaction host**:给引擎一个 **frozen getters 视图**(`thresholdRatio`/`retainTokens` 为 getter,读 `live()`),引擎每次 `resolveTargetPolicy()` 自然读到新值 —— 无需 re-apply、无需事件;保留 `retainRatio` 删除语义;advisory 窗口探测保留并降级为告警日志(0.1.7 无写校验 seam)。
35
+ - **client**:ns 常量改为 entry id(`FAILOVER_SETTINGS_ENTRY='dashr-failover'`、`COMPACTION_SETTINGS_ENTRY='dashr-compaction-tuning'`);UI locale/slot id 不变。
36
+ - **其它 0.1.7 破面清扫**:`agent/session-start` → **`agent/created`**(serial,dispatcher 注入 agent);ContentBlock **`'tool-result'` 移除** → role-`'tool'` 消息 + `message.isError`;session v4 source kind 在 `MessageSourceMap` 里声明 **`'plugin:better-dsh'`**;`SubagentResult.output` readonly 化。
37
+
38
+ ## 三、验证(验收 = 改动同类:真实运行时里设置面可用)
39
+
40
+ 环境:Dev/Test 1,upstream checkout 切 **dsh-v0.1.7-rc.1**,全量 `pnpm run build`,内嵌副本 = 本修复;`PORT=4988`(4999 被 superd PoC 占用)。
41
+
42
+ 1. **构建卫生**:`tsc --noEmit` **0 错**;vitest **600 passed / 1 skipped / 0 failed**(54 文件)。
43
+ 2. **第一人称 CDP 实测**(`.scratch/probe-049/verify-settings-049.mjs`,真实浏览器走 token URL → Settings → General):
44
+ - `Auto-compact threshold` 行 **可见**,stepper 在位,点击后 **无 "Save failed"**;
45
+ - failover 两个 select 目录填充(deepseek-official/*),选择 `deepseek-official/deepseek-flash` → **saveError: false**、**valueRetained: true**;
46
+ - `pageErrors: []`;截图 `.scratch/probe-049/settings-general.png`。
47
+ 3. **落盘证据**:`.dsh-test/profiles/web/cordis.patch.yml` 出现
48
+ `dashr-compaction-tuning.config.thresholdRatio: 0.75`(≠ 默认 0.8 = UI 写入)与
49
+ `dashr-failover.config.fallback1: deepseek-official/deepseek-flash`(= 探针所选)—— 0.1.7 的 entry-patch 持久化闭环成立。
50
+ 4. **回归探针**:boot graph 含 `better-dsh` + client.js URL;`/plugins/??better-dsh/client.js&rev=…` 200,与 `lib/client/index.js` 字节一致(服务端仅追加 sourceMappingURL)。
51
+
52
+ ## 四、本轮环境新账(全部已处理,后来者必读)
53
+
54
+ 1. **根 package.json 新 patch:`vite: ^6.0.0`** —— rc.1 起 `apps/desktop/tsdown.config.ts` 运行时 `import { build } from 'vite'`;本机 Node 走 unrun 加载 config,从 `.unrun` 缓存目录解析不到根 vite → build:lib 死。(上游 CI native loader 无此问题。)
55
+ 2. **孤儿残骸**:rc.1 删除了 `packages/settings/settings-file`,但 checkout -f 不清 gitignored `lib/` → tsdown workspace glob 吞进旧 `lib/types`(引用已不存在的 `SettingsProvider` 导出)→ build:lib 硬错。已 `rm -rf packages/settings/settings-file`。
56
+ 3. **cordis 身份**:副本 devDep `cordis: 4.0.2`(registry)与工作区全量的 **vendor/cordis 4.0.4** 是两个声明实例 → 增补(settings/tools/Events)分裂 → 212 个假错。副本改 `workspace:*` 后归一 → 0 错。sync 脚本已把 vendor 家族纳入手术名单。
57
+ 4. **同步脚本**:`.scratch/sync-better-dsh-copy.sh`(rsync → 手术 → install → tsdown → build-client → tsc)。手术名单 = `/tmp/workspace-provides.txt` ∪ `/tmp/vendor-provides.txt`(后者由 `vendor/*/package.json` 生成,易失,脚本失效时先重建)。
58
+ 5. **`installSection` grep 教训**:`git grep … | head` 截断曾让 0.1.6 树看起来"没有 installSection"——head 截断会伪造结论,长输出一律落文件再查。
59
+
60
+ ## 五、发布面(红线内,待 user 裁决)
61
+
62
+ - 修复只在 `align/v0.1.7-rc.1` 分支;**未 npm publish**(未获授权)。
63
+ - peer range `>=0.1.5 <0.2.0-0` 在 pnpm 预发布元组规则下**匹配不到 0.1.7-rc.1**(autoInstallPeers 会落到 0.1.5-rc.3)——发布前建议把 peer range 修成能覆盖 0.1.7 线的形状(如 `>=0.1.7-rc.1 <0.2.0-0` 或双分支),随下次发版定。
64
+ - prod 部署路径不变:user 以普通用户 `pnpm add @pgmi-builds/better-dsh@<新版>`(bundle 行)→ 重启 dsh.service。compaction/failover 两行在 prod 的用户层 patch 值会经 entry-patch 模型继续生效。
65
+ - 遗留小项:`dsh://config` 的 ns 现在是 entry id(`dsh://config/dashr-failover`);url-schemes 文档如提及旧 ns 需随发版更新。
@@ -0,0 +1,53 @@
1
+ # v0.2.4-b(未发版 HEAD)LSP mount 修复 + AST 可用性提醒 4999 实测报告
2
+
3
+ > 状态:**第一人称实测通过**(user 真实会话,2026-09-27);报告落位 = 发布闸 b 完成。
4
+ > 发布闸 c(user 单次确认发包)**待定**。
5
+ > 变更:change `2026-09-26-lsp-ast-reminder`(commit `f92a7cc`,含此前 HEAD 上未发布的
6
+ > `agent/created` mount 修复 `ce0541c`);计划 `docs/10_plans/2026-09-26-lsp-ast-reminder.md`。
7
+
8
+ ## 1. 被测内容
9
+
10
+ | 项 | 内容 |
11
+ |---|---|
12
+ | LSP gate mount 修复 | per-agent 安装事件 `agent/session-start`(0.1.7 已死)→ `agent/created`;修的是"hook 从未装上" |
13
+ | AST 可用性提醒 | `write/edit/grep` 结果尾部一行 `ast_edit / ast_grep available — …`;per-session cap 5(write/edit/grep 共享预算,agent dispose 重置);语言范围 = AST 自身 grammar 能力面(实测探针:py/js 家族/rs/go/c-cpp-h/html/css/json/yaml/sh/rb;排除 .md 与未证实 grammar),与 LSP 表解耦 |
14
+ | 交付形态 | 新规范(guide §4.2):`npm pack` → `plugin remove` → `plugin add tarball`;rig `test123`(`.test/home/compat`) |
15
+
16
+ ## 2. 构建卫生(agent 侧)
17
+
18
+ - 单测 **621 passed + 1 skipped**(新增 ast-reminder 单测 + post-execute 钩子级测试)
19
+ - `tsc --noEmit` host + client **0 错**;`npm run build` 干净
20
+ - rig 重启后 boot graph 含 `better-dsh`;cookie 认证 200(新规则 §3.3 生效实证)
21
+
22
+ ## 3. 第一人称实测(user,2026-09-27,rig 4999 真实会话)
23
+
24
+ **三个设备全绿**(user 实测原文要点):
25
+
26
+ - **ast_grep** ✅:`owners.set($K, $V)` 命中 2 处,含位置与 `metaVariables`;
27
+ - **ast_edit** ✅:dry-run 正确计算替换区间不落盘;`dryRun:false` 实改 scratch 文件
28
+ `add($A, $B)` → `add($B, $A)` 一次换两个调用点,`applied: true`,盘面核实后清理;
29
+ - **lsp(typescript-language-server)** ✅:diagnostics(精确命中 2×TS6133)/ hover
30
+ (返回接口 + doc comment)/ references(3 处,跨文件)/ definition / format
31
+ (`changed: true`)/ reload / status 全通。
32
+
33
+ **提醒链路(agent 侧客观证据,rig session log)**:user 验收会话
34
+ `session-d33f5db1`(superd workspace)工具结果尾部实测出现
35
+ **LSP nag ×1 + AST 提醒行 ×1**——mount 修复 + 提醒链路在真实运行时生效的直接证据。
36
+ (另一会话 `session-c8472342` 的 15 次 AST 行属**旧无 cap lib** 时段(今晨 04:59–05:08
37
+ HKT,早于调优构建 16:19 HKT),非 cap 违规;新 lib 时段计数正常。)
38
+
39
+ ## 4. 实测学到的 gotcha(攒批处理清单)
40
+
41
+ | # | 现象 | 候修 |
42
+ |---|---|---|
43
+ | G1 | **LSP 冷启动竞态**:server spawn 后首次 hover/references/definition 返回空(`text:""`/`locations:[]`),diagnostics 可预热,reload+重试确认 | 设备侧查询前 ensure/等待就绪,或空结果附 retryHint |
44
+ | G2 | **ast_grep/ast_edit 要求绝对路径**:workspace 相对路径 ENOENT | 相对路径按 workspace cwd 解析 |
45
+ | G3 | **lsp write 必须 `action` 字段**:usage 字符串的 `read:` 选择器语法易误导(`status`/`diagnostics?file=` 是 read 面) | usage 文案补 write 契约示例 |
46
+ | G4 | **`plugin remove` 曾停掉运行中的 rig unit**(journal "Stopping" 恰在 remove 时刻,观察 1 次) | 查 CLI remove/reconcile 是否有停 unit 副作用 |
47
+
48
+ ## 5. 发布闸状态
49
+
50
+ - a. 4999 第一人称实测 ✅(本文 §3)
51
+ - b. 报告落位 ✅(本文)
52
+ - c. **user 单次确认发包:待定**。HEAD = `v0.2.4b` + 19 commits(含 0.1.7 对齐修复、
53
+ 本 change);下一版号建议 `0.2.4c`(字母顺延),批量携带 G1–G4 修复与否由发布时定。
@@ -32,19 +32,12 @@ __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_pri
32
32
 
33
33
  //#region src/failover/config.ts
34
34
  /**
35
- * The `failover` general-fallback config (host-plane, per-turn latch).
36
- *
37
- * Pure module — no `@deepseek-ai/*` value imports, so both the host module and
38
- * the client half may import it (the client type-only) without dragging a host
39
- * runtime dependency into the browser bundle.
40
- *
41
- * A route is a `provider/model` string (e.g. `deepseek/deepseek-v4-flash`).
42
- * The empty string `''` means "not set" — the slot is skipped.
43
- *
44
- * @module dashr/failover/config
35
+ * The Loader entry id the failover row composes under. dsh 0.1.7 collapsed the
36
+ * settings namespace onto entry ids: `remote.settings` describe/update address
37
+ * the row's patch entry, the entry id IS the settings namespace, and edits
38
+ * persist as a profile-layer patch override through the config editor.
45
39
  */
46
- /** The settings namespace the General row reads and writes. */
47
- const FAILOVER_SETTINGS_NS = "failover";
40
+ const FAILOVER_SETTINGS_ENTRY = "dashr-failover";
48
41
 
49
42
  //#endregion
50
43
  //#region src/failover/client/locales.ts
@@ -430,6 +423,13 @@ function setupMobileLayout(ctx) {
430
423
  */
431
424
  /** The settings namespace the General row reads and writes. */
432
425
  const COMPACTION_SETTINGS_NS = "compaction-tuning";
426
+ /**
427
+ * The Loader entry id the tuning row composes under. dsh 0.1.7 collapsed the
428
+ * settings namespace onto entry ids: `remote.settings` describe/update address
429
+ * the row's patch entry, the entry id IS the settings namespace, and edits
430
+ * persist as a profile-layer patch override through the config editor.
431
+ */
432
+ const COMPACTION_SETTINGS_ENTRY = "dashr-compaction-tuning";
433
433
  /** Upstream `compaction-basic`'s own default: condense at 80% of the window. */
434
434
  const DEFAULT_THRESHOLD_RATIO = .8;
435
435
  /** Whole-percent bounds for the General row's stepper. */
@@ -587,7 +587,7 @@ function CompactionRow({ t, loadConfig, save }) {
587
587
  onClick: () => {
588
588
  step(THRESHOLD_STEP_PERCENT);
589
589
  },
590
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, { size: 9 })
590
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutlineRegular, { size: 9 })
591
591
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
592
592
  type: "button",
593
593
  className: CompactionRow_module_css_default.thresholdArrow,
@@ -596,7 +596,7 @@ function CompactionRow({ t, loadConfig, save }) {
596
596
  onClick: () => {
597
597
  step(-THRESHOLD_STEP_PERCENT);
598
598
  },
599
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 9 })
599
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: 9 })
600
600
  })]
601
601
  })]
602
602
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -632,7 +632,7 @@ function setupCompactionRow(ctx) {
632
632
  loadConfig: async () => {
633
633
  const response = await ctx.remote.settings.describe();
634
634
  if (!response.ok) throw new Error(`${response.error.code}: ${response.error.message}`);
635
- const view = response.value.namespaces.find((entry) => entry.ns === COMPACTION_SETTINGS_NS);
635
+ const view = response.value.namespaces.find((entry) => entry.ns === COMPACTION_SETTINGS_ENTRY);
636
636
  if (view === void 0) return null;
637
637
  const value = view.value;
638
638
  return {
@@ -641,7 +641,7 @@ function setupCompactionRow(ctx) {
641
641
  };
642
642
  },
643
643
  save: async (patch, revision) => {
644
- const response = await ctx.remote.settings.update(COMPACTION_SETTINGS_NS, patch, revision);
644
+ const response = await ctx.remote.settings.update(COMPACTION_SETTINGS_ENTRY, patch, revision);
645
645
  if (!response.ok) throw new Error(`${response.error.code}: ${response.error.message}`);
646
646
  return response.value.revision;
647
647
  }
@@ -686,7 +686,7 @@ function apply(ctx) {
686
686
  loadConfig: async () => {
687
687
  const response = await ctx.remote.settings.describe();
688
688
  if (!response.ok) throw new Error(`${response.error.code}: ${response.error.message}`);
689
- const view = response.value.namespaces.find((entry) => entry.ns === FAILOVER_SETTINGS_NS);
689
+ const view = response.value.namespaces.find((entry) => entry.ns === FAILOVER_SETTINGS_ENTRY);
690
690
  if (view === void 0) return null;
691
691
  const value = view.value;
692
692
  return {
@@ -696,7 +696,7 @@ function apply(ctx) {
696
696
  };
697
697
  },
698
698
  save: async (patch, revision) => {
699
- const response = await ctx.remote.settings.update(FAILOVER_SETTINGS_NS, patch, revision);
699
+ const response = await ctx.remote.settings.update(FAILOVER_SETTINGS_ENTRY, patch, revision);
700
700
  if (!response.ok) throw new Error(`${response.error.code}: ${response.error.message}`);
701
701
  return response.value.revision;
702
702
  }
@@ -1,4 +1,4 @@
1
- import { Context } from "@deepseek-ai/cordis";
1
+ import { Context, Volatile } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
 
4
4
  //#region src/compaction/config.d.ts
@@ -22,21 +22,31 @@ declare const name = "dashr-compaction-tuning";
22
22
  */
23
23
  declare const inject: string[];
24
24
  /**
25
- * The `compaction-tuning` settings schema.
25
+ * The row settings schema.
26
26
  *
27
- * `thresholdRatio` is the field the General row drives; `retainTokens` is
28
- * composition-only (see {@link DEFAULT_RETAIN_TOKENS}) but still validated and
29
- * applied, so the recency window keeps one source of truth.
27
+ * `thresholdRatio` is the field the General row drives and is volatile (dsh
28
+ * 0.1.7 settings model): editing it from Settings lands as a profile-layer
29
+ * patch override on this row and the resolved ref rewrites in place.
30
+ * `retainTokens` is composition-only (see {@link DEFAULT_RETAIN_TOKENS}) and
31
+ * stays non-volatile — form writes never touch it, a patch file still can.
30
32
  */
31
33
  declare const Config: z<CompactionTuningConfig>;
32
34
  /**
33
- * Install the settings namespace and keep the live compaction engine in sync.
35
+ * The row config as cordis hands it over post-resolution: the volatile
36
+ * threshold arrives as a live reference, not a plain number.
37
+ */
38
+ interface ResolvedCompactionTuningConfig {
39
+ thresholdRatio: number | Volatile<number>;
40
+ retainTokens: number;
41
+ }
42
+ /**
43
+ * Keep the live compaction engine on the user's threshold preference.
34
44
  * @param ctx - host context of the row that owns this capability.
35
- * @param config - composition base layer; schema defaults fill the rest.
45
+ * @param config - resolved row config; the volatile threshold is a live ref.
36
46
  */
37
- declare function installCompactionTuning(ctx: Context, config?: CompactionTuningConfig): void;
47
+ declare function installCompactionTuning(ctx: Context, config?: ResolvedCompactionTuningConfig): void;
38
48
  /** Mount the tuning as the row's whole apply (the row config is the settings base). */
39
- declare function apply(ctx: Context, config: CompactionTuningConfig | undefined): void;
49
+ declare function apply(ctx: Context, config: ResolvedCompactionTuningConfig | undefined): void;
40
50
  declare const _default: {
41
51
  name: string;
42
52
  inject: string[];
@@ -44,4 +54,4 @@ declare const _default: {
44
54
  apply: typeof apply;
45
55
  };
46
56
  //#endregion
47
- export { Config, apply, _default as default, inject, installCompactionTuning, name };
57
+ export { Config, ResolvedCompactionTuningConfig, apply, _default as default, inject, installCompactionTuning, name };