@huseynsnmz/pi-board 0.15.3 → 0.16.0

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 (211) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +53 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +276 -0
  7. package/package.json +5 -11
  8. package/src/index.ts +1 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/IMPLEMENTATION_PLAN.md +0 -920
  12. package/PRD.md +0 -484
  13. package/PROGRESS.md +0 -142
  14. package/VERIFY.md +0 -115
  15. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  16. package/docs/EXPLORATION.md +0 -187
  17. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  18. package/docs/RELEASE.md +0 -148
  19. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  20. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  21. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  22. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  23. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  24. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  25. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  26. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  27. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  28. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  29. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  30. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  31. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  32. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  33. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  34. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  35. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  36. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  37. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  38. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  39. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  40. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  41. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  42. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  43. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  44. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  45. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  46. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  47. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  48. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  49. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  50. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  51. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  52. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  53. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  54. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  55. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  56. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  57. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  58. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  59. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  60. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  61. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  62. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  63. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  64. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  65. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  66. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  67. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  68. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  69. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  70. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  71. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  72. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  73. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  74. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  75. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  76. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  77. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  78. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  79. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  80. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  81. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  82. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  83. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  84. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  85. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  86. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  87. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  88. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  89. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  90. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  91. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  92. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  93. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  94. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  95. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  96. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  97. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  98. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  99. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  100. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  101. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  102. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  103. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  104. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  105. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  106. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  107. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  108. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  109. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  110. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  111. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  112. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  113. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  114. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  115. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  116. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  117. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  118. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  119. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  120. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  121. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  122. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  123. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  124. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  125. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  126. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  127. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  128. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  129. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  130. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  131. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  132. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  133. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  134. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  135. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  136. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  137. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  138. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  139. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  140. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  141. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  142. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  143. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  144. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  145. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  146. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  147. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  148. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  149. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  150. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  151. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  152. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  153. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  154. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  155. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  156. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  157. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  158. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  159. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  160. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  161. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  162. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  163. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  164. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  165. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  166. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  167. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  168. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  169. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  170. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  171. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  172. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  173. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  174. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  175. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  176. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  177. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  178. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  179. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  180. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  181. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  182. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  183. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  184. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  185. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  186. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  187. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  188. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  189. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  190. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  191. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  192. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  193. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  194. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  195. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  196. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  197. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  198. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  199. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  200. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  201. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  202. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  203. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  204. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  205. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  206. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  207. package/scripts/budget-audit.mjs +0 -135
  208. package/scripts/release.mjs +0 -446
  209. package/scripts/release_helper.mjs +0 -309
  210. package/scripts/run-perf-gate.mjs +0 -40
  211. package/scripts/run-tests.mjs +0 -375
@@ -1,130 +0,0 @@
1
- # Spec: attach 运行期失同步检测 + 限速 heal 兜底(issue #11)
2
-
3
- - 日期:2026-09-07
4
- - 状态:approved(2026-09-08 用户批准;review v2 修订:检测改定时器入口、CJK 验证点、A4 可行性标注)
5
- - 范围校准:attach 期失同步已由 shrink-and-hold 协议(#25 + #42 G6)覆盖;本 spec 只做**运行期**(attach settle 后、会话进行中)的失同步检测与限速补救。
6
-
7
- ## 1. 背景
8
-
9
- issue #2 的修复建议 #2(失同步检测)在 #10 复盘中被证明必要:jiggle 链耗尽后系统再无自愈手段,脏画面(双光标+残留帧)一直挂着直到用户 detach/reattach。#25/#42 之后 attach 期已收敛,但运行期失同步(重放垃圾残留、子端渲染异常、未知失败模式)仍无兜底——本 spec 补上这道最后防线。
10
-
11
- ## 2. 失同步信号原理
12
-
13
- **正常态**:子端 pi-tui 每帧把 PTY 硬件光标定位到编辑器 marker 处,而 marker 处的编辑器假光标 cell 是反色的(`ESC[7m`)。因此**光标 cell 本身就是反色 cell**。
14
-
15
- **失同步态**:本地 headless xterm 的光标记账与 buffer 内容不一致(典型:光标停在差分写结束处、footer 段末尾),光标 cell 非反色,假光标留在编辑器行。
16
-
17
- **streaming 例外**:输出进行中光标落在输出行(非反色)是正常的。用「距上次 socket 输出 > DESYNC_QUIET_MS」区分——输出停止后子端最后一帧会把光标定位回编辑器。
18
-
19
- ## 3. 设计
20
-
21
- ### 3.1 失同步判定纯函数 `detectCursorDesync(buf, cursor)`
22
-
23
- 位置:`src/core/pty-attach-render.mjs`(纯函数、零依赖,随既有模块)。输入:xterm buffer(`getLine`/`getCell`/`isInverse` 最小接口)+ `projectPtyCursor()` 的返回值。返回三态:
24
-
25
- - `"aligned"`:cursor 非空且光标 cell 反色(含光标列越界但行末 cell 反色的等价形态)——正常;
26
- - `"misaligned"`:cursor 非空(光标在投影视口内)但光标 cell 非反色(cell 不存在、宽度 0、非反色均算)——候选失同步,等待时间门确认;
27
- - `"unknown"`:cursor 为 null(光标在视口外,如用户滚动历史)——不判定。
28
-
29
- 主判定 O(1)(只查光标 cell);仅在光标列越界需回看行末 cell 时退化 O(cols)。
30
-
31
- ### 3.2 controller 新增 `heal(cols, rows)`
32
-
33
- 位置:`src/core/pty-attach-jiggle-controller.mjs`。运行期补救入口,复用 shrink-and-hold 协议:
34
-
35
- 1. healBudget 检查:已达上限(`HEAL_MAX_PER_LIFETIME = 5`)→ return,不再补救;通过检查即消耗一次额度(含后续因极小终端放弃的情况——防反复尝试);
36
- 2. 清 timer;若有旧 hold(held)先 restore(幂等);
37
- 3. 重置检测状态(`state = createJiggleRetryState()`、carry 清空);
38
- 4. **保留 `tuiFrameSeen = true`**(运行期子端必然渲染过——这是与 `start()` 的关键区别,跳过 G1 无帧守卫与首帧 re-arm 分支);
39
- 5. 重新 shrink 并 hold(`holdSize = resizeJiggleSize(cols, rows)`;极小终端无 holdSize → 直接放弃本次 heal);
40
- 6. `scheduleNextRetry()` 复用现有退避表(G2 预算耗尽自动 restore)。
41
-
42
- 清屏检测复用 `feed()`:见 `\x1b[2J` → restore + 停链(现有逻辑)。守卫交互:
43
-
44
- - **G3**:`restoreAndStop()` 照常清 heal 的 hold;
45
- - **G4**:`notifyExternalResize()` 清 hold 与 timer,heal 自动取消(现有逻辑不动);
46
- - **G5**:`start()`(重连)先恢复旧 hold——heal 的 hold 同样被恢复,重连后协议重新走 attach 期自愈;
47
- - **预算**:healBudget 在 controller 实例生命周期内累计(`start()`/`restoreAndStop()` 均不重置),防断连循环骚扰。
48
-
49
- ### 3.3 组件接线(`src/ui/pty-attach.ts`)
50
-
51
- - `pushOutput()` **同步**收到数据时记录 `lastOutputAt`(不在 `term.write` 异步回调里记);
52
- - `attachSettled` 即现有 `!this.attaching` 字段(不新造计时器),settle 后才开始检测;
53
- - **检测入口是独立 `checkDesync()` 组件方法 + 低频定时器,不挂 render 路径**——render 是事件驱动的(socket 输出/keypress/resize),而失同步恰恰发生在输出停止后,挂在 render 路径上会让空闲态失同步永远没有检测机会。定时器在 attach settle 后启动(周期 `DESYNC_PROBE_INTERVAL_MS = 2000`),unref 不阻止进程退出,`close()` 时清理;
54
- - `checkDesync()` 内部用与 `project()` 相同的视口计算(start/height)调 `projectPtyCursor` + `detectCursorDesync`,7 个门全部通过才 heal:
55
- 1. `attachSettled === true`;
56
- 2. controller `tuiFrameSeen === true`(子端是 pi,shell/vim 不检测);
57
- 3. `detectCursorDesync(...) === "misaligned"`;
58
- 4. `Date.now() - lastOutputAt > DESYNC_QUIET_MS`(1500ms);
59
- 5. 链不活跃:`getState()` 满足 `state.stopped && !held`(heal/attach 链进行中不重复触发);
60
- 6. 距上次 heal > `HEAL_RATELIMIT_MS`(10000ms);
61
- 7. socket 连接存活(`this.connected`)。
62
- - 门 1-7 通过 → `jiggleRetry.heal(this.cols, this.rows)`,记录 `lastHealAt`。healBudget 上限不是组件层门:由 `heal()` 内部强制(预算耗尽直接拒绝),组件层无需重复检查。
63
-
64
- ### 3.4 数据流
65
-
66
- ```
67
- socket 输出 → pushOutput(记 lastOutputAt)→ term.write 异步解析
68
- ↘(输出停止后 render 不再触发)
69
- settle 后定时器(2s 周期)→ checkDesync()(自算视口 + projectPtyCursor)
70
- → detectCursorDesync → 7 门判定 → heal()
71
- → sendResize(shrink) → 子端 fullRender(true) 全清
72
- → feed() 检测 \x1b[2J → restore 原尺寸 → 自愈完成
73
- ```
74
-
75
- ## 4. 误报场景与防护
76
-
77
- | 场景 | 防护 |
78
- |------|------|
79
- | streaming 中光标在输出行 | 门 4 时间条件(1.5s 无输出才判) |
80
- | 全清重绘中间态 | 重绘=大量输出 → lastOutputAt 持续刷新 → 门 4 挡住 |
81
- | 非 pi 子进程(shell/vim,光标行永无反色) | 门 2 tuiFrameSeen |
82
- | attach settle 前(重放中间态/banner 遮挡) | 门 1 |
83
- | 用户滚动历史(光标在视口外) | detectCursorDesync 返回 unknown |
84
- | pi 静默等待外部输入(密码 prompt 等) | 无法完全防(真误报)→ healBudget=5 上限封顶,闪烁有限次后停止 |
85
- | heal 进行中重复检测 | 门 5 链活跃检查 |
86
-
87
- **已知漏报(接受)**:光标 cell 恰好反色但 buffer 其他处有残影——检测不到。兜底不追求完美。
88
-
89
- **假设**:子端 buffer 的反色 cell 主要来源是编辑器假光标(detach gate `findLastInverseCellLine` 已依赖同一假设)。若存在其他反色源,只影响漏报(把失同步误判为 aligned),不产生误报。
90
-
91
- **实现时验证点**:CJK 宽字符的 continuation cell(width 0)在 xterm 中是否继承首格的反色属性——若光标停在中文 continuation cell 上被判为非反色,会造成中文输入场景误报。实现时用真实 @xterm/headless 验证,并在 A1 补对应用例(若确实误判,检测逻辑需回看首格)。
92
-
93
- ## 5. 非目标
94
-
95
- - attach 期失同步检测(已由 shrink-and-hold 覆盖);
96
- - 连续补救失败后隐藏光标块 + 状态行提示(原 issue 可选兜底 3,以 healBudget 上限替代);
97
- - 残留帧特征检测(内容重复行等,成本高收益低);
98
- - Windows 实机行为(无实机,标 pending)。
99
-
100
- ## 6. 参数表
101
-
102
- | 参数 | 默认值 | 说明 |
103
- |------|--------|------|
104
- | DESYNC_QUIET_MS | 1500 | 判定失同步所需的输出静默窗口 |
105
- | DESYNC_PROBE_INTERVAL_MS | 2000 | settle 后定时检测周期 |
106
- | HEAL_RATELIMIT_MS | 10000 | 两次 heal 最小间隔 |
107
- | HEAL_MAX_PER_LIFETIME | 5 | controller 实例生命周期内 heal 总上限(进入 heal() 即消耗一次,含极小终端放弃的情况) |
108
-
109
- ## 7. 验收矩阵
110
-
111
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
112
- |----|--------|----------|----------|----------|
113
- | A1 | detectCursorDesync 三态判定 | 自动化(unit) | `node --test test/pty-attach-render.test.mjs` | aligned/misaligned/unknown 各场景断言正确(反色 cell、非反色、cell 越界、宽度 0、cursor null) |
114
- | A2 | heal() 协议行为 | 自动化(unit) | `node --test test/pty-attach-jiggle-controller.test.mjs` | shrink→feed clear→restore;budget 耗尽拒绝;G4 取消 heal;G5 恢复 heal hold;tuiFrameSeen 保留;极小终端放弃 |
115
- | A3 | 组件接线 + 7 门 + 自愈闭环 | 自动化(integration) | 新增 test-support TS 脚本(`--experimental-transform-types`,仿 detach-gate-smoke 模式)+ `node --test` 包装 | 伪造 buffer/输出流驱动真实组件:misaligned+静默→触发一次 heal resize;10s 内不重复;链活跃/settle 前/视口外/无帧均不触发;模拟子端全清输出后自愈停止;时间相关断言优先时间注入,其次真实等待 |
116
- | A4 | 健康 session 无误触发 | 自动化(E2E) | 复用 cold-start E2E harness 模式,真实 runner+pi 冷启动 attach 后稳定运行窗口内监控 heal 计数/resize 序列 | 正常会话全流程 heal 触发次数为 0。**可行性依赖**:现有 E2E harness 绕过组件胶水层(advisory 30-3),plan 阶段先核实组件级 harness 能力;不可行则降级为「A3 强化门控回归 + U1 观察」 |
117
- | U1 | 日常使用观察 | 用户实测 | 日常使用若干天:观察无误触发闪烁;(若偶遇真失同步)观察限速周期内自愈 | 无每 10s 频闪类骚扰性重绘;U1 为观察性验收,允许 pending,不阻塞合并 |
118
-
119
- ## 8. 可测性拆分设计
120
-
121
- - **detectCursorDesync(buf, cursor)**:纯函数,输入最小 buffer 接口(伪造 `getLine→getCell→isInverse` 对象,test/pty-attach-render.test.mjs 已有 BufferLineLike 伪造模式)。测试边界:只测函数本身的三态映射,不涉及时间/限速(属组件层)。
122
- - **heal()**:注入式 controller(fake `sendResize`/`setTimeoutFn`/`clearTimeoutFn`,既有模式)。测试边界:协议时序与守卫交互,不涉及检测信号。
123
- - **接线门控**:真实组件(TS)+ 伪造 tui/socket 数据流,test-support 脚本模式。测试边界:门的组合逻辑与端到端自愈闭环,不依赖真实子进程。
124
- - **A4 E2E**:真实 runner+pi,只断言「健康流无误触发」这一回归性质,不构造失同步(构造失同步属于 A3 的伪造层职责)。
125
-
126
- ## 9. 风险
127
-
128
- - pi 静默等待场景(密码 prompt/长工具无 spinner 帧)真误报 → 一次闪烁 + budget 封顶,无无限循环;
129
- - 子端其他反色源(若存在)→ 只漏报不误报;
130
- - 参数(1.5s/10s/5 次)为工程估值,A3/A4 验证后可调。
@@ -1,58 +0,0 @@
1
- # issue #89 spec:attach 界面 Ctrl+← detach 和弦
2
-
3
- 日期:2026-09-08 · 状态:已授权全自动推进(用户 2026-09-08 决策)
4
-
5
- ## 背景与问题
6
-
7
- attach 界面里编辑器非空时 `←` 被判为光标左移转发给 child(#66/#68/#69 门禁链的有意行为),用户不知道要先清空输入才能 `←` 退出,被困后只能 Ctrl+C/D 强退(连带 shutdown child Pi——实录一次事故导致 host 反复冷启动 7 次)。需要一个不与编辑冲突、始终可用的退出和弦。
8
-
9
- ## 核心设计(issue 已定稿,本 spec 为落地细化)
10
-
11
- ### D1:Ctrl+← 无条件 detach
12
- `src/ui/pty-attach.ts` `handleInput`:在 `Key.left` 分支**之前**新增:
13
- ```ts
14
- if (matchesKey(data, Key.ctrl("left"))) {
15
- // Explicit detach chord (issue #89): single ← is gated on editor state
16
- // (it doubles as cursor-left in a non-empty draft), so a user with a draft
17
- // had no way out. Ctrl+← is unambiguous intent — detach unconditionally,
18
- // regardless of editor state or socket liveness (same guarantee as the
19
- // disconnected-← escape, issue #48).
20
- this.detach();
21
- return;
22
- }
23
- ```
24
- - 放 Key.left 分支前:确保组合键不被单键分支截获(matchesKey 语义上两者不相交,但顺序防御更稳);
25
- - 无条件:和弦语义 = "我要退出",不需要门禁(门禁防的是误触,组合键无误触);
26
- - pi-tui Key 支持现成:keys.js L845-846 legacy(`\x1b[1;5D`)+ kitty 序列均映射 ctrl+left。
27
-
28
- ### D2:header 提示更新
29
- - L282:`← detach` → `←/Ctrl+← detach`
30
- - L298(renderLoading 中心提示):`← to detach` → `←/Ctrl+← to detach`
31
-
32
- ### 否决项(issue 已论证,记录防重提)
33
- - Esc:child 可能跑 vim/nvim,Esc 必须透传;
34
- - 双击 ←:编辑时连按 ← 移动光标是高频操作,必误触。
35
-
36
- ## 非目标
37
- - 不改 ← 单键门禁链任何行为(#66/#68/#69 的判定逻辑保持原样);
38
- - 不改其他键位;不改 detach() 本身语义。
39
-
40
- ## 可测性拆分设计
41
-
42
- | 单元 | 性质 | 测法 |
43
- |---|---|---|
44
- | Ctrl+← 分支 | 组件 handleInput | 扩建现有 `test-support/detach-gate-smoke.ts` harness(fake tui + send spy + didDetach,#42/#48/#66 同款):注入 `\x1b[1;5D` 序列 |
45
- | 文案 | render 输出 | harness 内 render(width) 断言含 "Ctrl+←" |
46
-
47
- ## 验收矩阵
48
-
49
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
50
- |----|--------|----------|----------|----------|
51
- | A1 | Ctrl+← 编辑器非空(draft)时 detach | 自动化(冒烟) | `node --test test/pty-attach-detach-gate.test.mjs` | didDetach() === true(editor_state draft 场景) |
52
- | A2 | Ctrl+← 编辑器空时 detach | 自动化(冒烟) | 同上 | didDetach() === true |
53
- | A3 | 单次 ← 门禁链回归 | 自动化(冒烟) | 同上 | 现有 15+ 断言全绿 |
54
- | A4 | header 文案 | 自动化(冒烟) | 同上 render 输出断言 | 含 "Ctrl+←" |
55
- | A5 | 全量回归 | 自动化(static/build) | `npm test` + `npm run typecheck` | 618+ 全绿 |
56
- | U1 | 真实场景 | 用户实测 | 合并后重启 pi:attach 活跃 session,输入几个字 → Ctrl+← | 立即 detach 回 dashboard,child 不受影响 |
57
-
58
- U1 需重启 pi,合并后用户执行。
@@ -1,52 +0,0 @@
1
- # issue #88 spec:dashboard 花屏修复(首帧全清 + 收缩自愈帧)
2
-
3
- 日期:2026-09-08 · 状态:已授权全自动推进(用户 2026-09-08 决策)
4
-
5
- ## 背景与问题
6
-
7
- dashboard(pi-tui 全屏 overlay)花屏:行重复、folder 计数错位。根因:pi-tui 差分渲染在 overlay 激活时禁用 clearOnShrink(tui-main-screen.js L315 `!hasOverlayEntries`),且首帧不清屏("assumes clean screen")——脏底/收缩残留无任何自愈通道。拖窗口(widthChanged → fullRender(true))可恢复,证明全清是有效兜底。973d492(v0.3.0)起存在的存量问题。
8
-
9
- ## 核心设计(单文件:src/ui/dashboard.ts)
10
-
11
- ### D1:mount 首帧全清
12
- 组件新增 `needsFullClear = true`;首帧 render 时 `this.tui.requestRender(true)`(nextTick 异步,无递归)——下一帧全量重绘,给干净底。
13
-
14
- ### D2:内容收缩自愈帧
15
- - `fitToHeight` 记录 pad 前内容行数到实例字段(pad 后行数恒满屏,检测无效——必须用 pad 前值);
16
- - `render(width)` 改为包装方法:调原逻辑(改名 `renderLines`)拿 lines → 若 `needsFullClear` 或内容行数较上帧**减少** → `requestRender(true)` → 更新记录 → 返回 lines;
17
- - 只响应**减少**(增长/同行数由差分正确处理),避免无意义全清。
18
-
19
- ### 为什么这样安全
20
- - `requestRender(true)` 经 nextTick 异步执行(tui.js L612-628),render() 内调用不递归;
21
- - 全清帧被 DECSET 2026 同步输出包裹,支持终端无闪烁;dashboard-render.mjs 注释警告的是"每帧 true",本设计仅首帧+收缩帧低频触发;
22
- - 不动 pi-tui 上游、不动差分语义、不动 dashboard-render.mjs。
23
-
24
- ## 非目标
25
- - pi-tui 上游修复(node_modules 不可控;且 overlay 禁 clearOnShrink 是有意设计);
26
- - 每帧全清(闪烁,明确放弃);
27
- - attach 视图(PtyAttachComponent)的渲染问题(不同组件,不在本 issue)。
28
-
29
- ## 可测性拆分设计
30
-
31
- | 单元 | 性质 | 测法 |
32
- |---|---|---|
33
- | 首帧/收缩触发逻辑 | 组件 render 包装(.ts) | 子进程冒烟:`test-support/dashboard-shrink-render.ts` 用 --experimental-transform-types 加载组件,fake tui spy + fake deps,render 三帧(首帧/增行/减行)输出 requestRender 调用序列 JSON |
34
- | 既有差分语义 | dashboard-render.mjs | 现有测试 "dashboard repaint preserves Pi TUI differential render state" 不动(requestDashboardRender 不改成 force) |
35
-
36
- 冒烟脚本 deps 构造照搬 `test-support/dashboard-refs-render.ts`(同组件既有范式)。
37
-
38
- ## 验收矩阵
39
-
40
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
41
- |----|--------|----------|----------|----------|
42
- | A1 | mount 首帧全清 | 自动化(integration/冒烟) | `node --test test/dashboard-render.test.mjs` | 首帧 render 后 requestRender 收到 [true] |
43
- | A2 | 收缩帧自愈 | 自动化(integration/冒烟) | 同上 | 减行帧触发 [true];增行/不变帧不触发 |
44
- | A3 | 修复在 .ts 层(jiti 可重载) | 自动化(static) | diff 审查 | 运行时代码改动仅在 dashboard.ts(.mjs 无运行时行为变更) |
45
- | A4 | 全量回归 | 自动化(static/build) | `npm test` + `npm run typecheck` | 617+ 全绿 |
46
- | U1 | 真实花屏场景 | 用户实测 | 合并后重启 pi:密集创建/删除若干 session(可配合 host 崩溃场景),观察 dashboard | 无行重复/计数错位残留;无需拖窗口恢复 |
47
-
48
- U1 需重启 pi(git 包不热重载),合并后用户执行。
49
-
50
- ## 风险与降级
51
- - 老终端无同步输出支持时全清帧可见一闪——低频可接受;
52
- - 若 U1 发现仍有残留场景(如运行期外部写屏非首帧非收缩),后续可加"定时低频全清"兜底,本 spec 不做。
@@ -1,87 +0,0 @@
1
- # issue #87 spec:legacy 死 host 安全回收(resolver 自愈)
2
-
3
- 日期:2026-09-08 · 状态:待用户确认
4
-
5
- ## 背景与问题
6
-
7
- v0.5.x 时代(无 instanceId 协议)的 legacy PTY host,当 runner 进程被异常杀死(SIGKILL / 承载终端关闭)时 host.json 永远停在 `state: "alive"`(或 `"starting"`)。v0.6.0 的 attach resolver 对 legacy host 执行「never recovered」(spec §10.1 保守决策),probe 失败直接 pending,同时 `hostActive`(纯磁盘状态)让 ensureHost 拒绝重新 claim——三层叠加成死锁,attach 永久失败。本机实录 6 个 view 处于该状态。
8
-
9
- ## 设计目标
10
-
11
- resolver 对「pid 可验证已死 + endpoint 不可达」的 legacy host 自动 finalize 为 `exited` 并走正常 ensure/claim 自愈,消除死锁;不满足安全条件时保持现有 pending 行为(不推翻 spec 的保守原意)。
12
-
13
- ## 核心设计
14
-
15
- ### D1:纯决策函数 `canFinalizeLegacyHost`(host-coordination.mjs)
16
-
17
- ```js
18
- /**
19
- * @param {{ host: HostStatus|null, hostPid: number|null, hostPidAlive: boolean,
20
- * probeClassification: string }} input
21
- * @returns {boolean}
22
- */
23
- ```
24
-
25
- 返回 true 当且仅当四条件同时成立:
26
- 1. `host.instanceId == null`(legacy)
27
- 2. `host.state ∈ {"starting", "alive"}`(stopping 由既有恢复路径 L907-933 覆盖,不重复处理)
28
- 3. `hostPid != null && hostPidAlive === false` —— pid 已死是核心安全证据;pid 复用只会让 isAlive=true → 走保守分支,方向安全
29
- 4. `probeClassification ∈ {"missing", "stale"}` —— missing=ENOENT(socket/pipe 不存在);stale=ECONNREFUSED+isSocket(进程死、socket 文件残留)。`unknown`/`occupied`/`starting` 一律不回收
30
-
31
- 纯函数、零副作用、零注入,真值表可全枚举测试。
32
-
33
- ### D2:resolver legacy 分支改造(service.mjs `resolveAttachTargetInner`)
34
-
35
- - pid 解析复用 loadRow 同款 fallback:`Object.hasOwn(host, "runnerPid") ? host.runnerPid : readHostPid(root, viewId)`(legacy 的 pid 在 host-pid.json 镜像)
36
- - 现 L963 `else if (legacy) return pending(...)` 改为:
37
- - `canFinalizeLegacyHost(...)` 为 true → finalize(见 D3)→ `continue`(下一轮 row 重载 hostActive=false → 走正常 ensure/claim)
38
- - 否则保持原 pending(行为不变)
39
- - `state === "starting"` 的 legacy(L936 `withinGrace` 恒 true 的等死分支):在 grace 等待分支内同样先查 `canFinalizeLegacyHost`(该分支 probe 已执行,classification 可得),满足即 finalize + continue,不再等 grace 到期
40
-
41
- ### D3:finalize 动作(service.mjs 内联,写路径唯一)
42
-
43
- `writeHost(root, viewId, { ...host, state: "exited", endedAt: now, lastSeenAt: now, error: "legacy host finalized: runner pid dead" })` + `appendDiagnostic({ source: "service", level: "info", code: "legacy_host_finalized", ... })`。
44
-
45
- - legacy 无 instanceId,不存在并发 owner,无需 fencing(与新协议 updateOwnedHost 路径区分);竞态窗口由 host-start lease 串行化兜底(claim 走全新 instanceId,与 exited 记录不冲突)。
46
- - 选 `exited` 而非 `failed`:进程是正常死亡语义(被外部杀死),failed 在现有代码里语义是"spawn/启动失败"(#86 场景),exited 与 v0.5.x runner 自然退出时写的状态一致,下游(canReplaceHost/ensure)对两者处理相同。
47
-
48
- ### D4:平台兼容性
49
-
50
- 不引入任何文件存在性检查(Windows 命名管道 existsSync 不可用,#45);证据只来自 probe classification(connect+hello 是唯一权威,spec §7.1)+ isAlive(kill(pid,0) 跨平台)。不碰 prewarm keypress 路径(2s TTL 纪律)。
51
-
52
- ## 非目标(明确排除)
53
-
54
- - **升级迁移扫描**(issue 建议 2):resolver 自愈后功能冗余(下次 attach 自然恢复);若后续要 dashboard 行状态立刻正确可单开 issue。
55
- - **UI 兜底提示**(issue 建议 3):永久 pending 状态被消除后无存在意义。
56
- - **stopping 状态 legacy**:已有恢复路径覆盖(L907-933 对 instanceId != null 生效;legacy stopping 的 `staleStop` 判定要求 instanceId != null——属现存另一个小缺口,本次不扩范围,记为遗留观察项)。
57
-
58
- ## 可测性拆分设计
59
-
60
- | 单元 | 位置 | 性质 | 测法 |
61
- |---|---|---|---|
62
- | `canFinalizeLegacyHost` | host-coordination.mjs 新增导出 | 纯函数 | 真值表单测:4 条件 × 关键组合(全满足/缺 pid/pid 活/unknown/stopping/新协议 host) |
63
- | pid fallback 解析 | service.mjs resolver 内联(复用 store.readHostPid) | 副作用隔离 | 集成测试造 legacy host.json(无 runnerPid 属性)+ host-pid.json |
64
- | finalize + 自愈闭环 | service.mjs resolver | 集成 | 注入 scriptProbe(missing) + 死 pid → 断言 host.json 落 exited、diagnostic 写入、resolver 继续 claim 新 host |
65
- | 保守分支 | 同上 | 集成 | pid 活(用 process.pid)→ 断言 pending 且不写盘 |
66
-
67
- 测试基建现成:`resolverService` + `scriptProbe` + `aliveHost` fixtures + `instantSleep`(test/host-resolver.test.mjs 模式)。`aliveHost` fixture 需支持造 legacy host(无 instanceId、无 runnerPid 属性 + host-pid.json 镜像),必要时加 `legacyHost` fixture helper。
68
-
69
- ## 验收矩阵
70
-
71
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
72
- |----|--------|----------|----------|----------|
73
- | A1 | canFinalizeLegacyHost 决策正确性 | 自动化(unit) | `node --test test/host-coordination.test.mjs` | 真值表全组合通过 |
74
- | A2 | legacy alive + pid 死 + missing → 自愈闭环 | 自动化(integration) | `node --test test/host-resolver.test.mjs` | host.json 落 exited、diagnostic 有 legacy_host_finalized、resolver 成功 claim 新 instance |
75
- | A3 | legacy alive + pid 活 → 不回收 | 自动化(integration) | 同上 | pending 返回、host.json 未被改写 |
76
- | A4 | legacy starting + pid 死 → 不等 grace 即回收 | 自动化(integration) | 同上 | 不 sleep 到 deadline 即完成回收 + claim |
77
- | A5 | probe unknown → 不回收 | 自动化(integration) | 同上 | pending、不写盘 |
78
- | A6 | 全量回归 | 自动化(static/build) | `npm test` + `npm run typecheck` | 568+ 全绿、无新类型错误 |
79
- | U1 | 本机 6 个真实卡死 legacy view 实测 | 用户实测 | 运行副本 checkout PR 分支 → 重启 pi → board 对卡死 view 按 enter | 全部自动拉起新 host 可正常 attach,无 manual restart 提示 |
80
-
81
- U1 必须用户执行(重启 pi 会断开实现 session)。执行时机:PR 合并前。
82
-
83
- ## 风险与降级
84
-
85
- - pid 复用误判方向恒为保守(不回收),不会误杀;
86
- - finalize 后若 claim 失败(如 PTY 不可用),行为与现有 ensure 失败路径一致(pending + 原因),无新增失败模式;
87
- - Windows 无 legacy named-pipe 实测环境(U2 类)——设计只依赖平台无关的 probe/isAlive,风险评估为低,最终报告标注未实测。
@@ -1,56 +0,0 @@
1
- # issue #86 spec:spawn 异步 error 兜底(spawnDetached 统一封装)
2
-
3
- 日期:2026-09-08 · 状态:已授权全自动推进(用户 2026-09-08 决策)
4
-
5
- ## 背景与问题
6
-
7
- `launch.mjs` 4 个启动函数与 `pty-attach.ts openExternalTarget()` 的 spawn 均为 `spawn(...)` + `unref()` 无 `'error'` listener。spawn 启动失败走异步 `'error'` 事件,EventEmitter 无 listener 的 error 直接 throw → uncaughtException → 整个 pi 宿主进程退出。实录:WSL2 上一次 node 二进制瞬时 ENOENT 直接带崩 pi(host.json 已正确落 failed,但进程没活下来走重试)。
8
-
9
- ## 核心设计
10
-
11
- ### D1:launch.mjs 新增内部 helper `spawnDetached`
12
-
13
- ```js
14
- function spawnDetached(command, args, cwd) {
15
- const child = spawn(command, args, { cwd, detached: true, stdio: "ignore", env: process.env, windowsHide: true });
16
- child.on("error", () => {}); // 接住异步启动失败;调用方按 pid==null 记 failed,宿主不崩
17
- child.unref();
18
- return child;
19
- }
20
- ```
21
-
22
- `launchRun` / `launchHost` / `launchTitle` / `launchAutoState` 四处收敛到该 helper。helper 不导出(模块内部实现细节,测试通过公开入口行为验证)。
23
-
24
- ### D2:openExternalTarget 三分支补 error listener
25
-
26
- darwin/win32/其他三分支统一为 `const child = spawn(...); child.on("error", () => {}); child.unref();`。fire-and-forget 语义与返回值不变(打开链接是 best-effort,失败静默)。参照同文件 L847-848 xclip 既有正确模式。
27
-
28
- ## 非目标
29
-
30
- - runner/*.mjs 内的 spawn(跑在分离 runner 进程里,崩了不拖垮 pi,issue 已明确排除);
31
- - 顶层 `process.on("uncaughtException")` 兜底(会掩盖未知错误,不引入);
32
- - 改变任何返回值/状态机语义。
33
-
34
- ## 可测性拆分设计
35
-
36
- | 单元 | 性质 | 测法 |
37
- |---|---|---|
38
- | spawnDetached 异步 error 兜底 | 副作用隔离(仅 child_process) | integration:注入不存在 node 路径,断言返回 pid:null 且进程存活(uncaughtException listener 不触发;node:test 进程崩=测试天然失败) |
39
- | 4 个公开入口行为不变 | 公开 API | 复用现有 4 测试 + 新增 ENOENT 用例覆盖 launchRun/launchHost/launchTitle/launchAutoState |
40
- | openExternalTarget | UI 层(c8 阈值外,惯例冒烟保护) | 静态验证 error listener 存在;不单加测试 |
41
-
42
- ## 验收矩阵
43
-
44
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
45
- |----|--------|----------|----------|----------|
46
- | A1 | spawn 失败不带崩进程(4 入口) | 自动化(integration) | `node --test test/launch.test.mjs` | 注入 `/nonexistent/node-ENOENT-test` 后各入口正常返回 pid:null,进程存活 |
47
- | A2 | 既有行为回归 | 自动化(integration) | `node --test test/launch.test.mjs` | 原 4 测试全绿 |
48
- | A3 | openExternalTarget error 兜底 | 自动化(static) | 代码审查 + grep 断言 | 三分支均有 error listener |
49
- | A4 | 全量回归 | 自动化(static/build) | `npm test` + `npm run typecheck` | 568+ 全绿、无类型错误 |
50
- | U1 | issue 复现脚本对照 | 自动化替代(A1 等价) | issue 自带复现脚本逻辑已并入 A1 | 见 A1 |
51
-
52
- U1 说明:issue 的复现脚本本质是"注入坏 node 路径 + uncaughtException 监听",A1 测试完全等价覆盖,故不需要独立用户实测。无 U 类纯人工项。
53
-
54
- ## 风险
55
-
56
- 极低:纯增量兜底,不改成功路径任何行为;失败路径从"进程崩"变为"走既有 pid==null 记 failed 重试路径"。
@@ -1,79 +0,0 @@
1
- # issue #90 spec:defaultModel 失效防护(launch 前校验 + child exit 归因)
2
-
3
- 日期:2026-09-08 · 状态:已授权全自动推进(用户 2026-09-08 决策)
4
-
5
- ## 背景与问题
6
-
7
- session 创建时记录的 `meta.defaultModel` 失效后(provider 前缀改名/移除),attach 走「launchHost → child --model 失效 → exit 1 → host exited → 再 attach 再 launch」死循环(实录单 view 数百次 launch_host)。且 runner child exit 不写 error 字段(pty-runner.mjs L217),host.json 只有 exitCode:1 无任何归因线索。
8
-
9
- ## 核心设计
10
-
11
- ### D1:launch 前模型校验(断死循环)
12
-
13
- 1. **core 导出匹配函数**(`src/core/launch-options.mjs`):
14
- ```js
15
- /**
16
- * Whether a stored model reference resolves to a currently-available model.
17
- * Same rule as the dashboard launch picker: case-insensitive exact "provider/id".
18
- * @param {string|null|undefined} modelRef
19
- * @param {Array<{provider: string, id: string}>|undefined|null} availableModels
20
- * @returns {boolean} true when modelRef is empty/null (no constraint) or matched.
21
- */
22
- export function modelRefAvailable(modelRef, availableModels)
23
- ```
24
- `modelRef` 空 → true(无校验对象);`availableModels` 空/undefined → true(调用方无法判断时保守放行,不制造新阻塞)。
25
-
26
- 2. **service 注入**:`createService` 新增 `opts.availableModels`(`() => Array<{provider,id}>`,每次调用实时取值;默认 undefined → 跳过校验)。`src/index.ts` serviceFor 与 `src/commands/agent-board.ts` flag 路径注入 `() => ctx.modelRegistry.getAvailable()`(try/catch → undefined,对齐 agent-board.ts L75-79 现有防御模式)。
27
-
28
- 3. **统一校验 helper**(service.mjs 模块内):`validateViewModelMeta(meta) → null | string`(null=通过;string=错误消息)。错误消息明确可行动:`Model "X" configured for this session is no longer available — update the view's model or clear defaultModel, then retry attach.`
29
-
30
- 4. **两个校验点**(service.mjs 仅有的 host spawn 路径):
31
- - `startHostUnderLease`:在 claim 之前校验(首选;若代码结构上 claim 已发生,则落 failed 清理后返回)→ `{ ok: false, error }`;
32
- - `adoptClaimedHost`:claim 是既有废弃记录 → 校验失败时 `updateOwnedHost` 落 failed(error 带模型消息)+ 返回 pending。
33
- - resolver 链路无需改动:ensureHostImpl 的 `{ ok:false, error }` 走现有 `pending(sessionFile, res.error)` → attach-flow notify 展示(渠道现成)。
34
-
35
- 5. **dashboard.ts L1738 `findLaunchModelByRef` 改为复用 core 导出**(单一事实源;LaunchModel 结构类型 JSDoc 化)。
36
-
37
- ### D2:child exit 错误归因(补线索)
38
-
39
- 1. **纯函数**(`src/core/heuristics.mjs` 新增导出):`lastVisibleLogLine(text, maxLen = 200)`——strip ANSI(CSI `\x1b\[[0-9;?]*[ -/]*[@-~]` + OSC `\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)` + 杂项转义),按行切分,返回最后一个非空可见行(`\r` 处理:取每行最后一个 `\r` 段),截断 maxLen。空输入/全空 → null。
40
- 2. **runner 接线**(`runner/pty-runner.mjs` child exit 回调 L213-223):`exitCode != null && exitCode !== 0` 时 best-effort(try/catch 包裹,绝不影响 exit 路径)读 screen.log 尾部 ~8KB(openSync/readSync seek 尾部,不整文件读)→ `lastVisibleLogLine` → 非 null 则 `update({ ..., error })`。exitCode=0 不改 error。
41
-
42
- ## 非目标
43
-
44
- - **失败退避**(issue 建议 2):D1 已断 spawn 死循环;通用退避需跨 claim 持久计数,复杂度/收益不划算,未来场景再单开;
45
- - **run 路径模型校验**(launchRun 的 model):detach run 失败是一次性的,不循环——观察项;
46
- - **UI 改动**:notify 渠道现成;
47
- - 自动 fallback 到其他模型:静默换模型会让用户不知 session 行为已变,fail-fast + 明确提示更安全(issue 建议 1 的两个选项中选 fail-fast)。
48
-
49
- ## 可测性拆分设计
50
-
51
- | 单元 | 位置 | 性质 | 测法 |
52
- |---|---|---|---|
53
- | `modelRefAvailable` | launch-options.mjs | 纯函数 | 真值表:大小写/null/空列表/部分匹配不匹配 |
54
- | `validateViewModelMeta` + ensure 路径 | service.mjs | 副作用隔离(注入 availableModels + launchHost spy) | integration:失效模型 → ok:false + launchHost 零调用 + 无 starting claim 残留;有效/null/未注入三对照 |
55
- | adopt 路径校验 | service.mjs | 同上 | integration:废弃 claim + 失效模型 → 落 failed + 不 spawn |
56
- | `lastVisibleLogLine` | heuristics.mjs | 纯函数 | 单测:ANSI/OSC/`\r`/空行/截断/空输入 |
57
- | runner exit 归因 | pty-runner.mjs | 集成 | pty-runner.integration.test.mjs 模式:fake child exit 1 + 预置 screen.log → host.json.error 含错误行;exit 0 → error 不变 |
58
-
59
- ## 验收矩阵
60
-
61
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
62
- |----|--------|----------|----------|----------|
63
- | A1 | modelRefAvailable 匹配规则 | 自动化(unit) | `node --test test/launch-options.test.mjs`(或既有对应文件) | 真值表全过 |
64
- | A2 | 失效模型 attach 不再 spawn | 自动化(integration) | `node --test test/host-resolver.test.mjs` / service 测试 | ok:false、launchHost spy 零调用、无 starting claim 残留、error 消息含模型名 |
65
- | A3 | 有效模型/null 模型回归 | 自动化(integration) | 同上 | 正常 launch(claim → spawn) |
66
- | A4 | 未注入 availableModels 向后兼容 | 自动化(integration) | 同上 | 跳过校验正常 launch |
67
- | A5 | adopt 路径失效模型 | 自动化(integration) | 同上 | 落 failed + 不 spawn |
68
- | A6 | lastVisibleLogLine 提取 | 自动化(unit) | heuristics 测试 | ANSI/截断/空行用例全过 |
69
- | A7 | runner exit≠0 归因 | 自动化(integration) | runner 测试 | host.json.error 含 screen.log 尾部错误行;exit 0 不写 |
70
- | A8 | 全量回归 | 自动化(static/build) | `npm test` + `npm run typecheck` | 608+ 全绿 |
71
- | U1 | 真实失效 view 实测 | 用户实测 | 合并后:view_539a5e9e20 恢复失效 defaultModel=glm/glm-5.3 → attach → 观察 notify 与 diagnostics | notify 明确提示模型失效;diagnostics 不再累积 launch_host;改回有效模型后 attach 成功 |
72
-
73
- U1 需重启 pi(git 包不热重载),合并后由用户执行并回 issue 记录。
74
-
75
- ## 风险与降级
76
-
77
- - availableModels 为空数组时保守放行(不制造新阻塞)——校验仅在能确定"失效"时拦截;
78
- - 校验失败路径每次 attach 仅一次文件级操作,无进程开销;
79
- - D2 全 try/catch best-effort,exit 路径行为不变。
@@ -1,147 +0,0 @@
1
- # Spec: exited host 的 claimPid 存活导致 attach 永久 pending(issue #99)
2
-
3
- ## 背景
4
-
5
- dashboard 里对已退出的 session 点 attach,永远连不上:attach 一直转圈,最终超时提示 `host start timed out`。实测对象 `view_2472d82627`(host 已正常退出 `state: "exited"`,`exitCode: 0`),同机另有 `view_4b667ad75d`、`view_c038badb30` 两个 view 处于相同状态(exited + claimPid 存活),全部无法 attach。
6
-
7
- ## 根因
8
-
9
- ### 触发链条
10
-
11
- 1. 用户从 dashboard(进程 P)attach session → `claimHost`(`src/core/store.mjs:172`)把 `claimPid` 记为 **dashboard 进程的 pid**(`claimPid: provisionalHost.claimPid ?? null`,即 service 进程 pid)
12
- 2. pi 子进程正常退出(`pty-runner.mjs:262` `child.onExit` → `state: "exited"`)→ **退出路径不清除 claimPid**
13
- 3. dashboard 进程 P 继续存活(用户一直开着 dashboard)
14
- 4. 再次 attach → `startHostUnderLease`(`service.mjs:243`)替换 terminal host 前调 `canReplaceHost(observeHostForReplace(existing))`
15
- 5. `observeHostForReplace`(`service.mjs:2006`)对 claim 角色用 `conservativeObservation(host.claimPid)`:**pid 活着 → `"unknown"`**
16
- 6. `canReplaceHost`(`src/core/host-coordination.mjs:72`)要求 runner / child / claim 三角色都 `SAFE_TO_RELEASE`(`not_started | dead | foreign`),claim 为 `"unknown"` → **返回 false**
17
- 7. → `pendingLaunchResult` → attach 循环等待直到 deadline → `pending(sessionFile, "host start timed out")`
18
-
19
- 实机验证(node 直接调用 `canReplaceHost`):
20
-
21
- ```
22
- host.state: exited
23
- runnerPid 1004724 → dead
24
- childPid null → dead
25
- claimPid 1003423 → unknown ← 阻塞点
26
- canReplaceHost → false
27
- ```
28
-
29
- ### claimPid 的语义(代码注释 + 测试确认)
30
-
31
- - **非 null = "launcher 可能还在 claim 和 spawn 之间"**:保护 mid-transaction,让 ensureHost/adopt 等 grace 窗口(`service.mjs:976` `withinGrace` 判定依赖 `claimPid != null`)
32
- - **recovery claim 用 `claimPid: null`**(`service.mjs:723-736`):recovery 事务在 claim 落盘时已完成,spawning 是 adopter 的活;`host-recovery.test.mjs:317` 断言 `recovery claim must not carry a live claimPid`
33
- - **spawn 失败路径清除 claimPid**(`service.mjs:832, 869`):failed fenced
34
- - **正常退出路径不清除** ← 缺口:host 进入 `exited` 后 claimPid 残留,而 claim 保护语义(launcher mid-transaction)在 host 已 terminal 时**不可能成立**(runner 都跑完退出了)
35
-
36
- ### 为什么是 bug
37
-
38
- `canReplaceHost` 的 claim 角色检查在 host 已 terminal 的场景下过度保守。claim 保护只对 `starting` 状态有意义;host 为 `exited/failed` 时,claim 进程不可能还在启动它(启动要么成功——runner 跑过并退出,要么失败——failed fenced 已清 claimPid)。触发条件常见:**dashboard 进程存活 + host exited → 任何从 dashboard 启动又退出的 session 都无法再次 attach**。
39
-
40
- ## 修复方案(选定:放宽 canReplaceHost 的 claim 角色判定)
41
-
42
- ### 方案对比
43
-
44
- | 方案 | 改动 | 对存量坏记录 | 评价 |
45
- |------|------|-------------|------|
46
- | **A. canReplaceHost 放宽 claim 判定** | 纯函数(`host-coordination.mjs`)+ 调用方(`service.mjs`) | **立即生效**(下次 attach 即可替换) | ✅ 选定 |
47
- | B. terminal 时清除 claimPid | 改 pty-runner 退出路径多处 + recoverHost finalize | 无效(已存在的坏记录不会自动修复,需额外迁移机制) | 改动面大、覆盖不全 |
48
- | C. conservativeObservation 增加 host 状态感知 | 改观测函数签名(传入 host 状态) | 立即生效 | 污染通用观测函数语义:`conservativeObservation` 的职责是"保守判断单个 pid 是否活着",让它感知 host 状态会把生命周期决策混进观测层,违背 service.mjs 里观测与决策分离的既有结构 |
49
-
50
- ### 设计
51
-
52
- **1. `canReplaceHost`(`src/core/host-coordination.mjs:72`)判定改为**:host 已 terminal(exited/failed)+ runner/child 均 provably gone + 无 launch lease → 可替换,claim 角色不参与判定。
53
-
54
- ```js
55
- export function canReplaceHost({ host, runnerObservation, childObservation, launchLeaseActive }) {
56
- if (!host || (host.state !== "exited" && host.state !== "failed")) return false;
57
- if (launchLeaseActive) return false;
58
- // claim 角色不参与判定:claim 保护语义(launcher mid-transaction)只在 host
59
- // 处于 starting 时有意义;host 已 terminal 时 claim 进程不可能还在启动它
60
- // (启动要么成功——runner 跑过并退出,要么失败——failed fenced 已清 claimPid)。
61
- return (
62
- SAFE_TO_RELEASE.has(runnerObservation) &&
63
- SAFE_TO_RELEASE.has(childObservation)
64
- );
65
- }
66
- ```
67
-
68
- **2. 同步移除 `claimObservation` 参数**(而非保留 unused 参数):
69
-
70
- - `canReplaceHost` 签名从 `{host, runnerObservation, childObservation, claimObservation, launchLeaseActive}` 改为 `{host, runnerObservation, childObservation, launchLeaseActive}`
71
- - `observeHostForReplace`(`service.mjs:2006`,内部函数不导出)返回值移除 `claimObservation` 字段——少一次 `process.kill(pid, 0)` 系统调用
72
- - 既有测试用例同步移除 `claimObservation` 参数
73
-
74
- **取舍论证**(为什么移除而非保留 unused):
75
-
76
- - `observeHostForReplace` 只有**一个**调用方(`service.mjs:243` 传给 `canReplaceHost`),且该函数是 service.mjs 内部函数不导出——无外部兼容性问题
77
- - 保留一个不参与判定的参数会让测试产生误导(传 `claimObservation: "unknown"` 的用例看起来期望 false,实际被忽略)
78
- - 移除后观测层少一次无用的 `isAlive` 系统调用
79
-
80
- ### 改动文件
81
-
82
- | 文件 | 改动 |
83
- |------|------|
84
- | `src/core/host-coordination.mjs` | `canReplaceHost` 判定放宽 + 签名移除 `claimObservation` + JSDoc 更新 |
85
- | `src/runtime/service.mjs` | `observeHostForReplace` 返回值移除 `claimObservation` 字段 |
86
- | `test/host-coordination.test.mjs` | 既有 3 用例移除 `claimObservation` 参数;新增 A1-A5 用例 |
87
- | `test/host-resolver.test.mjs` | 新增 A6 集成测试 |
88
-
89
- ### 安全性论证
90
-
91
- | 场景 | 分析 | 结论 |
92
- |------|------|------|
93
- | exited + claimPid 活着 | 只可能来自 runner 跑完退出(claim 事务早已完成) | 安全,**修复目标** |
94
- | failed + claimPid 活着 | runner 崩溃 / child spawn 失败(claim 事务已完成或 failed fenced 已清);child 存活时 childObservation 仍阻塞 | 安全 |
95
- | runner 活着 | runnerObservation = "unknown" → 仍阻塞(不受影响) | 保守性保留 |
96
- | child 活着 | childObservation = "unknown" → 仍阻塞(不受影响) | 保守性保留 |
97
- | alive/starting host + claimPid 存活 | attach 直接到现有 host(probe ready → 返回 pty),**不经过 canReplaceHost** | 不受影响 |
98
- | recoverHost 并发 | recoverHost 与 startHostUnderLease 都在 host-start lease 内执行,互斥 | 无竞争 |
99
- | starting 状态 | canReplaceHost 对非 terminal 直接返回 false(不进入新判定) | 不受影响 |
100
-
101
- ### 非目标
102
-
103
- - 不改 pty-runner 退出路径(方案 B 不做)
104
- - 不做存量 host.json 迁移/清理(方案 A 对存量记录天然生效)
105
- - 不改 `conservativeObservation`(其保守语义在 starting 场景仍需要)
106
- - 不改 dashboard UI、不改 host.json 结构(不加新字段)
107
-
108
- ## 验收矩阵
109
-
110
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
111
- |----|--------|----------|----------|----------|
112
- | A1 | canReplaceHost:exited/failed + runner/child dead + **claimPid 存活** → 可替换(修复点) | 自动化验证(unit) | `node --test test/host-coordination.test.mjs` | 新增用例通过:`{host:{state:"exited"}, runner:"dead", child:"dead", lease:false}` → `true`;`{host:{state:"failed"}, ...}` 同 → `true` |
113
- | A2 | canReplaceHost:runner/child 任一 unknown 仍阻塞(保守性不破坏) | 自动化验证(unit) | 同上 | `runnerObservation:"unknown"` 或 `childObservation:"unknown"` 时返回 `false` |
114
- | A3 | canReplaceHost:launchLeaseActive 仍阻塞 | 自动化验证(unit) | 同上 | `launchLeaseActive: true` 时返回 `false`(既有用例回归) |
115
- | A4 | canReplaceHost:非 terminal host(starting/alive/stopping)仍拒绝 | 自动化验证(unit) | 同上 | 三个状态均返回 `false`(新增断言) |
116
- | A5 | canReplaceHost:SAFE_TO_RELEASE 边界回归——foreign/not_started → 可替换;host null → false | 自动化验证(unit) | 同上 | `runnerObservation:"foreign"` 或 `childObservation:"not_started"` 时返回 `true`;`host: null` 返回 `false` |
117
- | A6 | resolver 集成:exited host + 存活 claimPid → attach 启动新 host | 自动化验证(integration) | `node --test test/host-resolver.test.mjs` | 新增用例:构造 exited host 记录(`claimPid: process.pid`,存活),`resolveAttachTarget` 返回 `{kind:"pty"}` 且 spawn 恰好 1 次 |
118
- | U1 | 真实 dashboard 场景:attach 已退出 session(dashboard 进程存活) | 用户实测 | 见下方步骤 | attach 成功进入 session,历史消息正常显示,无超时提示 |
119
-
120
- **U1 实测步骤**:
121
-
122
- 1. **重启 dashboard 进程**(加载新代码——方案 A 对存量坏记录的"立即生效"以重启为前提)
123
- 2. 打开 dashboard,找到已知坏记录 `view_2472d82627`(issue-225 session,host `state: "exited"` + claimPid 存活)
124
- 3. 点 attach → 观察:成功进入 session、历史消息正常显示、无 `host start timed out` 提示
125
- 4. 退出 session,再 attach,确认可重复
126
- 5. 同法验证 `view_4b667ad75d` / `view_c038badb30`(另两个 exited + claimPid 存活的 view)
127
-
128
- ## 可测性拆分设计
129
-
130
- 修复集中在 `canReplaceHost` 一个纯函数(`src/core/host-coordination.mjs`),无副作用、无 I/O,天然可单测:
131
-
132
- - **纯函数边界**:`canReplaceHost({host, runnerObservation, childObservation, launchLeaseActive})` → boolean。输入为纯数据快照,输出只依赖输入,不触碰 fs/进程/socket。
133
- - **观测与决策分离**:`observeHostForReplace`(`service.mjs:2006`)负责进程观测(`conservativeObservation`),`canReplaceHost` 负责决策。修复只动决策层;观测层移除 `claimObservation` 字段是配套清理(少一次无用的 `isAlive` 调用),不改变观测语义。
134
- - **测试边界**:
135
- - `test/host-coordination.test.mjs`:A1-A5 全部在纯函数层覆盖(现有 `canReplaceHost refuses unknown observations` 用例扩展 + 新增用例)
136
- - `test/host-resolver.test.mjs`:A6 走现有 `resolverService` + `healServiceOverrides` 基建(真实 ensureHostImpl claim + scripted probe),验证 attach 全链路(resolver → ensureHost → startHostUnderLease → canReplaceHost → spawn)
137
-
138
- ## 风险与降级
139
-
140
- - **行为变化面**:仅放宽"exited/failed host 的替换判定"一个点;starting/alive/stopping 的 host 不经过 `canReplaceHost`;runner/child 存活的 host 仍阻塞。
141
- - **回归风险**:低。改动为纯函数内一个条件的放宽 + 配套签名清理,A2-A5 保证保守性不破坏。
142
- - **降级路径**:若 U1 实测发现异常,**revert 本 issue 的 commit 恢复原判定**(claim 角色重新参与判定)。
143
-
144
- ## 环境
145
-
146
- - Linux x64,node v24.13.0,pi-agent-board main(0.6.1)
147
- - 2026-09-09 实机排查,`view_2472d82627` 现场取证(host.json / diagnostics.jsonl / ps 进程树)