@huseynsnmz/pi-board 0.15.1 → 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 (215) hide show
  1. package/CHANGELOG.md +26 -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 +18 -19
  8. package/src/index.ts +9 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/src/ui/double-left.ts +5 -1
  12. package/src/ui/focus.ts +7 -1
  13. package/src/ui/waiting.ts +10 -5
  14. package/IMPLEMENTATION_PLAN.md +0 -920
  15. package/PRD.md +0 -484
  16. package/PROGRESS.md +0 -142
  17. package/VERIFY.md +0 -115
  18. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  19. package/docs/EXPLORATION.md +0 -187
  20. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  21. package/docs/RELEASE.md +0 -148
  22. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  23. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  24. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  25. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  26. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  27. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  28. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  29. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  30. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  31. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  32. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  33. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  34. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  35. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  36. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  37. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  38. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  39. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  40. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  41. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  42. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  43. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  44. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  45. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  46. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  47. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  48. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  49. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  50. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  51. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  52. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  53. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  54. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  55. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  56. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  57. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  58. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  59. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  60. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  61. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  62. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  63. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  64. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  65. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  66. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  67. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  68. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  69. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  70. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  71. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  72. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  73. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  74. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  75. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  76. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  77. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  78. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  79. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  80. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  81. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  82. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  83. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  84. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  85. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  86. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  87. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  88. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  89. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  90. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  91. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  92. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  93. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  94. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  95. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  96. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  97. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  98. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  99. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  100. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  101. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  102. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  103. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  104. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  105. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  106. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  107. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  108. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  109. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  110. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  111. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  112. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  113. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  114. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  115. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  116. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  117. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  118. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  119. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  120. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  121. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  122. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  123. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  124. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  125. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  126. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  127. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  128. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  129. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  130. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  131. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  132. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  133. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  134. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  135. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  136. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  137. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  138. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  139. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  140. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  141. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  142. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  143. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  144. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  146. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  147. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  148. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  149. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  150. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  151. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  152. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  153. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  154. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  155. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  156. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  157. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  158. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  159. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  160. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  161. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  162. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  163. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  164. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  165. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  166. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  167. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  168. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  169. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  170. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  171. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  172. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  173. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  174. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  175. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  176. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  177. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  178. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  179. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  180. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  181. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  182. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  183. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  184. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  185. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  186. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  187. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  188. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  189. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  190. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  191. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  192. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  193. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  194. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  195. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  196. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  197. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  198. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  199. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  200. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  201. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  202. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  203. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  204. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  205. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  206. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  207. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  208. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  209. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  210. package/scripts/budget-audit.mjs +0 -135
  211. package/scripts/patch-vulns.mjs +0 -59
  212. package/scripts/release.mjs +0 -446
  213. package/scripts/release_helper.mjs +0 -309
  214. package/scripts/run-perf-gate.mjs +0 -40
  215. package/scripts/run-tests.mjs +0 -375
@@ -1,146 +0,0 @@
1
- # issue #106 spec:终态链(clear/settled)仍须学习帧认知
2
-
3
- 日期:2026-09-14 · 状态:设计已确认(用户 2026-09-14 决策,范围 B:两个终态一起覆盖)
4
-
5
- ## 根因(机制链确认)
6
-
7
- `src/core/pty-attach-jiggle-controller.mjs` 的 `feed()` 把「帧认知学习」放在两条终态早退之后:
8
-
9
- ```js
10
- 220: if (state.clearDetected) return; // chain done; nothing left to detect
11
- 221: if (state.stopped) return; // chain ended (G2/G3/G4); output is inert
12
- 222: const result = feedOutput(state, data, carry);
13
- ...
14
- 229: if (result.frameStartFound) tuiFrameSeen = true; // ← 唯一学习点(start() 的 :186 是唯一重置点)
15
- ```
16
-
17
- 因此,**链在第一个 TUI 帧之前进入终态 ⇒ 本次连接内 `tuiFrameSeen` 恒为 false**(写入点全仓只有 186/229 两处)。
18
-
19
- 后果链:`tuiFrameSeen === false` ⇒ `checkDesync()` 的 gate 2 早退(`src/ui/pty-attach.ts:590`)⇒ `heal()` 不可达(其唯一调用点 `pty-attach.ts:601` 在 gate 2 之后)⇒ 运行期失配自愈(#11)对本连接静默失效。纯漏报:不闪屏、不误触发,只是「该治不治」。
20
-
21
- ## 两个终态的可达性(含 `clearDetected` 的依据)
22
-
23
- | 终态 | 进入条件(代码事实) | 现实场景 |
24
- |---|---|---|
25
- | `stopped` | G2:退避表 8 轮耗尽(120ms…20s,累计 **56.12s**)期间既无 `\x1b[2J` 也无 `\x1b[?2026h`;G4:`notifyExternalResize`(`pty-attach.ts:969`)在首帧前到达 | 子进程是 shell(无 TUI 帧);慢启动 TUI 56s 内一帧未出;attach 后立刻拖窗口 |
26
- | `clearDetected` | live 输出出现 `\x1b[2J` 而此前/同 chunk 无 `\x1b[?2026h` | shell 里 `clear`、alt-screen 程序退出清屏等,之后**同一 session 内**起 TUI(例如 shell 里敲 pi) |
27
-
28
- `clearDetected` 是对 issue 正文(只点了 `stopped`)的扩展,依据是同一守卫区同形状、后果相同(gate 2 恒关)。
29
-
30
- **一处事实纠正**:`#11` 的永久笔记与 682f016 的代码注释称「screen-log replay 混合历史 2J + 2026h」。核实:`replayScreenLog()`(`pty-attach.ts:1091`)只调 `pushOutput()`,replay **不喂 controller**;`checkClearSequence()`(唯一调 `jiggleRetry.feed`,`:981`)只在 `onSocketData` 的 live `output` 分支被调(`:1050`)。所以 clear-wins 的真实来源是 **live 的 fullRender 输出块**(pi-tui 在同一同步更新块内发 `2026h … 2J …`),与 #106 的两个终态同源同路径。
31
-
32
- **复位路径(故障窗口边界)**:只有 `start()`(socket connect,`pty-attach.ts:427`)全复位 `clearDetected`/`stopped`/`tuiFrameSeen`/`carry`;`heal()`(保留认知,且认知缺失时不可达)、`restoreAndStop()`(G3)、`notifyExternalResize()`(G4)都不复位终态。⇒ **窗口 = 本次连接的整个生命周期**,「用户 reattach 一下就好」不成立(#11 的诉求正是无需用户干预)。
33
-
34
- ## 复现(确定性,无需 56s 真实等待)
35
-
36
- 单测层用现成的 `fakeScheduler` 直接 fire 完 G1 + 8 轮退避 → 链进 `stopped`(现有用例 `no re-arm probe after chain exhausted (stopped)` 已示范这条驱动路径);再 `feed("\x1b[?2026h late frame")` → 当前代码下 `tuiFrameSeen` 仍为 false(红)。`clearDetected` 分支用一次 `feed("\x1b[2J…")` 即可到达。
37
-
38
- 组件层用 `test-support/desync-heal-smoke.ts` 的既有基座(真 `PtyAttachComponent` + fake TUI + 注入 clock/connected + 真实 socket-data 路径)复现:终态后喂帧 → 当前代码下 `checkDesync()` 不 heal(红)。
39
-
40
- ## 修复设计
41
-
42
- 改动只有一个文件、一个函数:`src/core/pty-attach-jiggle-controller.mjs` 的 `feed()`(`pty-attach-jiggle-retry.mjs` 纯层不动——`feedOutput()` 早已返回 `frameStartFound` + `carry`,终态扫描所需信息齐备)。
43
-
44
- ```js
45
- function feed(data) {
46
- // Terminal chain states: a clear was seen (chain done) or the chain ended
47
- // (G2/G3/G4). Output no longer drives the retry protocol — but frame
48
- // cognition must still be learned from it (issue #106): a TUI whose first
49
- // frame lands after the chain settled must still open the runtime desync
50
- // backstop's gate 2 (issue #11), otherwise heal() stays unreachable for the
51
- // rest of this connection. Cognition only — no timers, no resizes, and no
52
- // retry-state change: re-opening the protocol is heal()'s job (rate-limited
53
- // and lifetime-capped), not an output chunk's.
54
- if (state.clearDetected || state.stopped) {
55
- if (tuiFrameSeen) return; // latched already — nothing left to learn
56
- const terminal = feedOutput(state, data, carry);
57
- carry = terminal.carry; // keep cross-chunk marker detection intact
58
- if (terminal.frameStartFound) tuiFrameSeen = true;
59
- return;
60
- }
61
- const result = feedOutput(state, data, carry);
62
- ...(以下原样不动)
63
- ```
64
-
65
- ### 为什么这样切
66
-
67
- - **合并两个终态为一个分支**:两者对重试协议的含义相同(链已结束),对认知学习的需求相同;分开写会复制同一段学习逻辑。
68
- - **`if (tuiFrameSeen) return;` 前置短路**:终态里认知已锁存后,每个 chunk 的两次 `includes` 扫描是无用功(惰性 shell session 会长期持续输出);短路后稳态成本为零,且 `carry` 也不再需要推进。
69
- - **`carry` 仍要推进(仅在未锁存时)**:帧标记 `\x1b[?2026h` 8 字节可能跨 chunk 边界,不推进会漏学(「只学一半」的假修)。
70
- - **不改 retry state**:终态是「协议已结束」的判定,不能被后续输出改写(否则 G2 的预算语义、`no re-arm probe` 契约都被推翻)。
71
-
72
- ### 契约(终态分支的行为边界)
73
-
74
- | 允许 | 禁止 |
75
- |---|---|
76
- | 读 `data`/`carry`,锁存 `tuiFrameSeen`,推进 `carry` | 设/清任何 timer(`chainTimer`/`g1Timer`) |
77
- | 立即返回 | 发任何 resize |
78
- | | 改 `state`(`clearDetected`/`stopped`/`retryIndex`) |
79
-
80
- ### 数据流(修复后)
81
-
82
- ```
83
- live 输出 → onSocketData → checkClearSequence → feed()
84
- ├─ 链活跃:原逻辑(clear 优先 / 首帧 fast-path / F1 慢启动探针)
85
- └─ 终态 :未锁存 → feedOutput 扫一遍 → 命中 2026h 即 tuiFrameSeen=true → 返回
86
- ↓
87
- desync 探针(2s)→ checkDesync → gate 2 通过 → 后续门(链空闲/静默/限速/失配)
88
- ↓
89
- heal() → 重新 shrink-and-hold → 子端 fullRender 自愈
90
- ```
91
-
92
- ### 风险与取舍
93
-
94
- | 风险 | 评估 |
95
- |---|---|
96
- | 误判面扩大(终态后 latch 的程序不一定是 pi-tui,如 vim 等使用 DECSET 2026 的程序) | 与「链活跃期内 latch」的既有语义完全一致(同一子进程若早 10 秒出帧就会被 latch)。heal 后续仍有 gate 3(光标真失配)+ 静默 + 10s 限速 + 5 次终身上限;伤害面最坏是一次重绘 |
97
- | 惰性链上持续扫描的开销 | 仅发生在「终态 + 认知未锁存」窗口;锁存后立即短路。窗口内每 chunk 成本 = 一次字符串拼接 + 两次 `includes` |
98
- | 把 G2 的「预算耗尽 = 放弃」语义变松 | 不变:终态分支不发 resize、不重开协议,重打开仍由 heal() 的独立预算把关 |
99
-
100
- ## 备选方案(考虑并否决)
101
-
102
- | 备选 | 否决理由 |
103
- |---|---|
104
- | 只在 `stopped` 早退前学习(issue 原文范围) | `clearDetected` 同形状同后果,分两次改动要重复复核同一守卫区(用户已选 B) |
105
- | 把学习点整体移到 `feed()` 最前面(无条件先学,再走各分支) | 等价于 682f016 的写法,但会把「链活跃」路径的学习语义也一并改写(`firstFrame` 与 clear 分支的相对顺序),改动面大于必要;本次只在终态新开一条零副作用通路,既有分支逐字不动 |
106
- | 终态后由 `feed()` 直接走 F1 式 re-arm(重开 hold) | F1 的前提是「预算未耗尽」(`ensureHold` 守卫含 `state.stopped`);在终态里 re-arm 等于绕过 G2 的预算判定,把「放弃」改成「无限重试」。重打开应交由 heal()(有独立限速与上限) |
107
- | 让 `checkDesync` 的 gate 2 改用别的信号(如「子端曾是 TUI」的其它痕迹) | 无更可靠信号;`tuiFrameSeen` 是现有唯一「子端确实在渲染帧」的证据,改判据会引入误报面(对 shell 子进程 heal 是明确要避免的) |
108
- | 改 `heal()` 使其可在无认知时自举 | 同上:会让 shell 子进程进入 heal 路径,直接违反 gate 2 的设计意图 |
109
-
110
- ## 非目标
111
-
112
- - 不改 G1/G2/G4 的守卫语义、退避表、`heal()` 预算与限速参数。
113
- - 不改 clear-wins 语义(同 chunk 内 clear 仍优先决定 re-arm)。
114
- - 不为「非 TUI 子进程」打开任何 heal 通路(无帧 ⇒ 仍不 heal,H4 语义保持)。
115
- - 不把 56s 的 G2 窗口改短/改长(#106 与窗口长度无关,只与「窗口结束后能否再学」有关)。
116
-
117
- ## 可测性拆分设计(自动化验证类功能点)
118
-
119
- 维持现有边界,**不新增抽象**:`pty-attach-jiggle-retry.mjs`(纯函数层:`feedOutput`/`advanceRetry`/`stopRetry`…)不动;`createJiggleRetryController(deps)` 是「状态机 + 注入副作用(`sendResize`/`setTimeoutFn`/`clearTimeoutFn`)」的既有拆分。观测点 = `getState()` 与测试记录下的 `resizes`/scheduler timer 集合——足以证明「认知锁存」与「零副作用」两个断言维度。
120
-
121
- | 功能点 | 独立单元 | 测试边界 |
122
- |---|---|---|
123
- | 终态认知锁存(两个终态) | `feed()` 终态分支(输入 `data`/`carry`/`tuiFrameSeen`;**无副作用**:不调 `sendResize`、不设 timer) | controller 单测(`fakeScheduler` + `resizes`):断言 `getState().tuiFrameSeen` 翻 true、`resizes.length` 不变、timer 集合不变、`stopped`/`clearDetected` 不变 |
124
- | 跨 chunk 帧标记拼接 | `feedOutput()` 的 `carry` 语义(既有纯函数,不改) | controller 单测:先喂半截 `\x1b[?202`(不锁存)→ 再喂 `6h`(锁存) |
125
- | 认知锁存后 heal 可达 | `heal()` 既有入口 | controller 单测:终态 + 锁存 → `heal(cols,rows) === true` 且 `held === true` |
126
- | 组件级 gate 2 真打开 → 真触发一次 heal | `PtyAttachComponent.checkDesync`(注入 `nowFn`/`connected`/`finishAttachTransition`) | `test-support/desync-heal-smoke.ts` 新场景 H8(走真实 socket-data 路径:`pushOutput` + `checkClearSequence`),断言 `healCount === 1` 且 resize 次数 = 1 |
127
- | 无帧子进程仍不 heal(回归) | 同上(H4 既有场景) | smoke H4 保持 `false`(不喂帧 ⇒ 学不到 ⇒ gate 2 仍挡) |
128
-
129
- ## 验收矩阵
130
-
131
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
132
- |----|--------|----------|----------|----------|
133
- | A1 | `stopped` 终态后帧认知仍锁存,且零副作用 | 自动化(unit) | `node --test test/pty-attach-jiggle-controller.test.mjs`(新增用例) | 预算耗尽 → feed 帧 → `tuiFrameSeen=true`;`resizes` 不变;无新增 timer;`stopped` 仍 true |
134
- | A2 | `clearDetected` 终态后帧认知仍锁存,且零副作用 | 自动化(unit) | 同上 | clear-only 终态(无帧)→ feed 帧 → `tuiFrameSeen=true`;`resizes` 不变 |
135
- | A3 | 跨 chunk 拆分的帧标记在终态下仍被 `carry` 接住 | 自动化(unit) | 同上 | 半截标记不锁存;补齐后锁存 |
136
- | A4 | 锁存后 `heal()` 可达(gate 2 打开) | 自动化(unit) | 同上 | `heal(cols,rows) === true`,`held === true`,`tuiFrameSeen` 保持 true |
137
- | A5 | 组件级:终态(shell,无帧)后晚到帧 → 真触发一次 heal | 自动化(integration) | `node --test test/pty-attach-desync-heal.test.mjs`(新增 H8 断言) | `healCount === 1` 且 resize = 1;H4(无帧)仍为 false |
138
- | A6 | 零副作用契约(终态后不发 resize)回归 | 自动化(unit) | 同 A1 | 既有 `no re-arm probe after chain exhausted (stopped)` 保持绿(27 条 controller 用例全绿) |
139
- | A7 | 静态与类型 | 自动化(static) | `npm run typecheck` | 无错误 |
140
- | A8 | 全量回归 + 打包 | 自动化(build) | `npm test`、`npm run verify`(含 `test:coverage`、`pack:dry`) | 全绿,无新增未覆盖分支告警 |
141
- | A9 | 红/绿自证(TDD 纪律) | 自动化(unit + integration) | 先在未改源码上跑 A1–A5 断言 | 修复前必失败(红),修复后必通过(绿);red 证据记入 PR |
142
- | U1 | 真实 session 观察性验收 | 用户实测(**非阻塞**) | 1) attach 到一个 shell 型/长静默 session;2) 等链走完预算(>60s)或先 resize 一次;3) 在该 session 内启动 pi;4) 观察是否出现周期性全清重绘 | 无误触发闪烁;若发生真失配,限速周期内自愈。**pending 理由**:需真实 ≥56s 无帧窗口 + 人为构造失配,脚本化成本高、收益低;与 #11 的 U1 同性质,合并观察 |
143
-
144
- ## 附:随修复一并提交的文档纠正(可单独 revert)
145
-
146
- 同一 doc-comment 块内的这句陈述不准确:「a clear only proves the child redraws, not that it isn't a TUI (**screen-log replay bundles historical frames with clears**, issue #11)」。依据:replay 不喂 controller(`replayScreenLog` 只调 `pushOutput`),真实来源是 live 的 fullRender 输出块。建议改为「a live fullRender chunk bundles a frame start with its clear」。理由:错误的成因描述会把后续排查引向 replay 路径。**若 review 认为应保持最小 diff,可单独 revert 这一句,不影响修复正确性。**
@@ -1,116 +0,0 @@
1
- # Issue 113 Spec — coordinator 写入竞态:前台预览被 agent_end 空投影覆盖
2
-
3
- > Issue: zhuxixi/pi-agent-board#113
4
- > 修订版(v2):吸收 spec 审查 findings(归档站点纠错、测试隔离、测试保真度、规则证据链、备选否决记录、字段边界、U1 前提)。
5
-
6
- ## 根因(机制链确认)
7
-
8
- `syncForegroundEvent` → `syncRowEvent` 每个事件开头 `statusFromRow(row)` **从磁盘 state.json 重新重建**内存 status(`listRows → loadRow → readState` 直读磁盘,已逐行确认)。`message_end` 设置 `latestAssistantPreview`/`lastAgentActivityAt` 后经 `void writeForegroundState(...)` **fire-and-forget** 发往 coordinator;coordinator 先 fsync journal 再 materialize(毫秒级延迟)。`projectViewState` 对 `latestAssistantPreview` 无 previousState fallback(空串 `""` 不被 `??` 遮挡),因此 7ms 后到达的 `agent_end` 重读到未落盘的旧 state → 重建出空投影 → `finalizeRun` 的 `deriveSummary` 兜底 "Needs instructions" → **await 写入把非空 preview 覆盖为 `""`**。
9
-
10
- 影响面:`sync_foreground` 仅 service.mjs 发送(交互式前台镜像),evidence.json 直写不受影响。0.6.x 子进程直写磁盘(read-your-writes 天然成立),coordinator 间接层(socket RTT + fsync-before-materialize)破坏该不变量。
11
-
12
- 正交路径(已排查,确认不是同 bug 变体):手动完成 fence(`isManualCompletion`,agent_end 分支先行返回);`reconcile()` 的 `reconcile_finalize`(service.mjs:2006)是 PTY host 崩溃恢复判定,payload 不含 preview。
13
-
14
- ## 修复设计(主修复 = issue 评论区已验证 patch 的规范落地)
15
-
16
- ### 新模块 `src/core/foreground-preview-cache.mjs`
17
-
18
- 进程内 read-your-writes 缓存,**模块级单例**——依据:`src/index.ts:88` 的 `serviceFor(ctx)` 无记忆化、每次调用都 `createService`("reuse one instance" 注释只针对 sweeper 的 `sweepService`,因其构造副作用),闭包级缓存会被每事件新建的实例丢弃。
19
-
20
- ```js
21
- export function createForegroundPreviewCache() {
22
- const map = new Map(); // viewId -> { latestAssistantPreview, lastAgentActivityAt }
23
- return {
24
- // 记住投影中的已知字段。新鲜度规则(以 lastAgentActivityAt 为序):
25
- // 更新时间戳的投影整体获胜;较旧/无时间戳的投影只补空缺,永不降级已有条目。
26
- remember(viewId, { latestAssistantPreview, lastAgentActivityAt }),
27
- // 回填:缓存时间戳严格更新于重建 status(含 status 无时间戳的 legacy 行)时
28
- // 整体采纳缓存两字段;否则仅填空字段(磁盘更新或同静时磁盘优先)。
29
- backfill(viewId, status),
30
- forget(viewId), // 归档时清理,防无界增长
31
- clear(), // 测试隔离:清空全部条目(模块级状态跨测试存活)
32
- size, // 测试/诊断用
33
- };
34
- }
35
- export const foregroundPreviewCache = createForegroundPreviewCache(); // 模块级单例
36
- ```
37
-
38
- 判定谓词(与仓库既有约定一致):`latestAssistantPreview` 判空用 falsy(空串);`lastAgentActivityAt` 判空用 `== null`。纯逻辑规则可独立测试,副作用(Map 读写)集中在模块内。
39
-
40
- ### 规则依据(为什么"空值永不覆盖"是安全的)
41
-
42
- 全仓 `latestAssistantPreview` 赋值只有两处,且都只会写非空值:
43
-
44
- - `src/core/events.mjs:132`——`message_end` 分支,位于 `if (text)` 内,只写 `truncate(text)`;
45
- - `src/core/state-commands.mjs:331`——`finalize_run` overlay,其唯一发送方 `runner/job-runner.mjs:272` 用 `if (status.latestAssistantPreview) payload...` 的 truthiness guard 只带非空值。
46
-
47
- 即"非空才带"已是本仓库处理该字段同类时序问题的既有约定,本修复把它延伸到前台镜像路径。此外 viewId↔sessionFile 映射不可变(全仓 src/runner/scripts 无 `.sessionFile =` 赋值;adopt 同文件复用同一 view、新文件新建 viewId),故按 viewId 缓存不会跨会话串值。
48
-
49
- **证据链 caveat(终审补充)**:`runner/job-runner.mjs:743` 还有一条不经 truthiness guard 的写入路径——`patch_fields` 命令携带 `state: { summary, latestAssistantPreview }`(字段白名单在 `state-commands.mjs:112`)。该写入方(后台 runner)不在前台缓存的覆盖范围内:缓存只在本进程 `writeForegroundState` 时 remember。因此回填必须"仅当缓存严格更新时才整体采纳"——绝不能用较旧的缓存值覆盖磁盘上更新的 runner 写入(实现已按此新鲜度规则落地)。
50
-
51
- ### `src/runtime/service.mjs` 改动点
52
-
53
- 1. `writeForegroundState`:`projected` 计算后、发送/直写前 → `foregroundPreviewCache.remember(row.meta.id, projected)`(coordinator 开/关两模式都执行;直写模式下 backfill 天然 no-op → 零行为变化)。
54
- 2. `syncRowEvent`:`statusFromRow(row)` 之后立即 `foregroundPreviewCache.backfill(row.meta.id, status)`(在所有事件分支之前——agent_end 分支的 `finalizeRun`/`deriveSummary`、throughput 分支的投影都从回填后的 status 出发)。
55
- 3. 归档清理(**仅两个真实写入点**,全仓 `archived = true` 的写入清单):
56
- - `service.mjs:670`(`archiveView`,`archiveMany`/`archive` 均委托到此)
57
- - `service.mjs:1968`(`archiveByState`,内联写 `row.meta.archived = true`,不经过 archiveView)
58
-
59
- 两处均加 `foregroundPreviewCache.forget(row.meta.id)`。
60
-
61
- ### 数据流(修复后,竞态时序)
62
-
63
- ```
64
- message_end: 磁盘读(空) → reduceEvent 设 preview=B → writeForegroundState 投影(B) → remember(v, {B}) → 发送
65
- agent_end: 磁盘读(空 preview) → backfill(v, status) 回填 B → finalizeRun → summary=首句B → 投影(B) → 发送 ✓
66
- ```
67
-
68
- ### 边界与降级
69
-
70
- - coordinator off(直写):`writeState` 同步落盘,磁盘读回自己的写 → backfill 不触发,行为与 0.6.x 一致。
71
- - 多轮会话:同一竞态在第 N≥2 轮下磁盘已有上一轮非空 preview,若只"填空"会把行冻结在上一轮回复(终审发现)。落地规则为新鲜度感知合并:缓存条目时间戳严格更新于磁盘重建值时整体采纳,较旧的重建写(agent_end baseline)也不得降级缓存;磁盘更新或同静时磁盘优先。
72
- - 多进程:缓存在单进程内;竞态发生在同进程事件流内,足够。
73
- - 已知边界(非本次修复范围):coordinator 命令到达乱序(0.7.0 架构既有行为,概率极低,窗口远小于 0.6.x 并发直写)。
74
- - 内存:viewId→2 字段;两个归档站点 forget;模块导出 `clear()` 供测试复位。上限不做额外 LRU(归档清理已覆盖生命周期)。
75
-
76
- ## 备选方案(考虑并否决,记录理由)
77
-
78
- | 备选 | 否决理由 |
79
- |---|---|
80
- | 方案 2:agent_end finalize 复用本轮 message_end 内存投影 | 需跨事件保留整个 status 对象;status 其他字段(lastActivityAt 等)依赖磁盘实时性,易引入新偏差;改动面大 |
81
- | 方案 3:coordinator `sync_foreground` 合并「非空最后写入获胜」 | 改单写者层覆盖语义 = 翻 #107/#91 的契约与测试;且不解决本进程重读决策用旧数据的问题;无第二发送方,暂不引入 |
82
- | 方案 4:per-view await 在途写入(通用 read-your-writes) | 需在每事件前 await 上一笔未落盘写:coordinator 降级时 ensure 窗口最长约 10s,会直接拖慢事件处理;且在发版前改动事件时序语义面过大。保留为未来若出现第二类字段回退问题时的候选 |
83
-
84
- ## 非目标
85
-
86
- - 同类 stale rebuild 理论上可回退的其它字段(如 `error` 的清除)不在本规则覆盖范围:「非空获胜」规则修不了 clear 型回退,套用会让陈旧 error 更黏(属设计上不能做的事)。当前无证据表明其造成用户可见问题。
87
- - 多轮乱序到达边界(既有架构行为,非本 issue 回归)。
88
- - legacy 路径行为变更:仅回归验证,不做主动改动。
89
- - coordinator 层防护(方案 3):见上表。
90
-
91
- ## 可测性拆分设计(自动化验证类功能点)
92
-
93
- | 功能点 | 独立单元 | 测试边界 |
94
- |---|---|---|
95
- | 缓存写入规则(新鲜度胜出、旧写不降级、空值不覆盖非空) | `remember`(覆盖语义) | 新模块单测:构造实例直接断言规则 |
96
- | 回填规则(严格更新时整体采纳;否则仅填空字段,磁盘优先) | `backfill`(字段级选择) | 同上:更新/较旧/同静/无时间戳四类输入 |
97
- | 事件流集成(message_end→agent_end 竞态) | `createService(opts.sendStateCommand)` 注入 fake | service.test.mjs 新用例:fake **延迟 N ms 落盘**(保真真实时序:晚到的 message_end 写不得回退 agent_end 的正确投影),断言最终磁盘状态 |
98
- | 真实 coordinator 不变式 | 既有 `startTrackedCoordinator` + `waitFor` 基座 | service.test.mjs 新用例(同型先例::862 真实 coordinator 前台测试):断言不变式,不依赖竞态是否发生 |
99
- | 归档清理 | `forget` + 两个归档站点 | 模块单测 + archiveView/archiveByState 路径断言 |
100
- | 测试隔离 | 模块 `clear()` | 新用例以 beforeEach/finally 复位模块级缓存(service.test.mjs 大量测试共用 viewId "v1",不复位会跨测污染、掩盖回归) |
101
-
102
- 关键:**延迟落盘 fake** 使竞态确定性复现(第二事件在延迟窗口内同步读取旧磁盘),且覆盖"晚到写入不回退"的真实语义——无 sleep 竞态、非 flaky。红证:未打缓存修复时 A3 必失败;绿证:修复后必通过。
103
-
104
- ## 验收矩阵
105
-
106
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
107
- |----|--------|----------|----------|----------|
108
- | A1 | 缓存写入规则(新鲜度胜出、旧写不降级、空值不覆盖非空) | 自动化(unit) | `node --test test/foreground-preview-cache.test.mjs` | 规则表断言全部通过 |
109
- | A2 | 回填规则(严格更新时整体采纳;否则仅填空,磁盘优先;preview 用 falsy、时间戳用 `== null`) | 自动化(unit) | 同上 | 四类输入断言通过 |
110
- | A3 | 竞态事件流:延迟落盘 fake,message_end→agent_end | 自动化(integration) | `node --test test/service.test.mjs`(新增用例) | 最终磁盘 state:preview=回复文本、summary=首句(非 "Needs instructions");移除缓存修复时该用例失败(红/绿自证);调用方式与生产一致(每次 `service(root)` 新建实例,验证缓存确在模块级) |
111
- | A4 | 真实 coordinator 不变式 | 自动化(integration) | `node --test test/service.test.mjs`(新增用例,复用 startTrackedCoordinator/waitFor) | 最终磁盘 preview/summary 正确(任何时序下成立) |
112
- | A5 | 直写模式不回归 | 自动化(unit 回归) | `node --test test/service.test.mjs`(既有 syncForegroundEvent 用例) | 全部通过,行为不变 |
113
- | A6 | 归档清理(两个站点) | 自动化(unit + integration) | 缓存单测 + `archiveView`/`archiveByState` 路径断言 | 归档后缓存无该 viewId |
114
- | A7 | 测试隔离 | 自动化(integration) | 新用例 beforeEach/finally 调用 `clear()` | 跨测试无残留(viewId "v1" 复用时行为一致) |
115
- | A8 | 全量回归 | 自动化(build) | `npm run typecheck && npm test` | 全绿 |
116
- | U1 | 真实交互会话端到端 | 用户实测 | 安装后开交互式 pi 会话,完成一轮带 assistant 回复的对话,等 idle,观察 dashboard | idle 行显示回复首句,非 "Needs instructions";state-journal.jsonl 无空投影覆盖序列(**前提**:修复进程内生效,仅对新代码加载后的事件有效;存量已写坏的行需等下一轮 message_end 才会显示正确预览,不追溯治愈) |
@@ -1,84 +0,0 @@
1
- # Spec:coordinator 管道名对 root 字符串形式敏感(issue #124)
2
-
3
- 日期:2026-09-15 · 状态:approved(用户确认)
4
- 调研:issue 评论 R1(根因确认 + 修复可行性 + 向后兼容实测)
5
-
6
- ## 1. 根因(systematic-debugging 结论)
7
-
8
- **根因**:`coordinatorEndpointPathFor`(src/core/paths.mjs:101-107)在 win32 上以 `sha256(String(root))`——root **原始字符串**——作为命名管道名,不做任何归一化;而同一逻辑 root 的**锁路径**经由 `path.join` 归一化。两个键对 root 形式敏感度不一致 → 同一逻辑 root 的两种写法(`C:/x` vs `C:\x`、尾分隔符、`.` 段)= **同一把锁 + 两根不同管道**。
9
-
10
- **触发条件**:任何绕过 `defaultRoot()`(`src/index.ts:27`,`path.resolve` 归一化)的 root 传入。`runner/state-coordinator.mjs:88` 直接 `const root = process.argv[2]`(原样使用)—— 手动/外部以不同形式启动 coordinator 即触发。
11
-
12
- **症状**:面板按自身形式 probe → ENOENT → 反复 spawn 新实例 → 新实例抢锁失败(锁被占)→ 面板永久 `coordinator_unavailable`;占锁实例永远收不到面板命令。现场误导性强(coordinator 活着、心跳正常、ping 通),易与 #114 混淆。
13
-
14
- **证据链**:
15
- 1. 实机复现:面板 root = 反斜杠(`AGENT_BOARD_ROOT=C:\...` → `path.resolve`),手动启动用正斜杠 argv → 两根管道(`0af89859…` / `8b9d0e3c…`)+ 同一把锁;
16
- 2. 同机实测:两形式 endpoint 不同(issue 正文表格),锁路径相同;
17
- 3. 代码静态核对:paths.mjs 三个 endpoint 中**只有** coordinator 以 root 为键(control 用 viewId、host 用 instanceId);
18
- 4. 归一化实测:4 种写法折叠为同一 hash,且**规范形式 hash 不变**。
19
-
20
- ## 2. 修复设计
21
-
22
- ### 2.1 归一化 hash 输入(D1)
23
-
24
- `src/core/paths.mjs`:
25
-
26
- ```js
27
- export function coordinatorEndpointPathFor(platform, root) {
28
- if (platform === "win32") {
29
- // Normalize before hashing: the pipe name must be invariant to the root's
30
- // spelling (C:/x vs C:\x, trailing separators, dot segments). The lock path
31
- // derived from the same root already is (via path.join); a mismatch yields
32
- // "same lock, two pipes" — panel permanently locked out (issue #124).
33
- // resolve() is idempotent on canonical roots, so already-deployed
34
- // coordinators keep their pipe name and need no restart.
35
- const normalized = path.win32.resolve(root);
36
- const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
37
- return `\\\\.\\pipe\\agent-board-coordinator-${hash}`;
38
- }
39
- return path.join(root, "coordinator.sock");
40
- }
41
- ```
42
-
43
- - **win32 语义显式指定**:hash 前用 `path.win32.resolve` 而非宿主 `path.resolve` —— 该分支必须在任何宿主 OS 上产出同一管道名(Linux/macOS 宿主的 `path.resolve` 是 POSIX 语义,会把 `C:\x` 当作相对路径:没有盘符与反斜杠分隔符语义),使 platform 注入式单测与 CI 在任何 OS 上行为一致;Windows 宿主上 `path === path.win32`,输出逐字节不变。
44
- - **可测性拆分**:该函数已是纯函数(platform + root → string,零副作用、无 I/O)→ 直接单测,无需新增抽象;副作用(bind/connect)留在 client/runner。
45
- - **向后兼容(实测)**:`path.resolve(canonical) === canonical` 成立 → 规范形式(生产 `defaultRoot()` 的产物)hash **不变** → 已部署 coordinator 管道名不变、零中断(实测数据见 issue R1 评论)。
46
- - **POSIX 分支不改**:其 endpoint 是文件系统路径,`path.join` 已归一化;改为 `path.resolve` 会让相对 root 语义变化(行为改变而非修复)。
47
-
48
- ### 2.2 测试(D2)
49
-
50
- **unit —— `test/socket-path.test.mjs`**(该函数专属测试文件,platform 参数注入 → 跨平台可跑):
51
-
52
- 1. win32:同一逻辑 root 的多种写法(`C:/r`、`C:\\r`、`C:\\r\\`、`C:\\.\\r`)→ **同一**管道名;
53
- 2. win32:不同 root → 不同管道名(per-root 隔离保留);
54
- 3. win32:前缀与长度约束(`\\.\pipe\agent-board-coordinator-` + 16 hex,≤256);
55
- 4. linux/darwin:返回 `path.join(root, "coordinator.sock")`,行为与修复前一致;
56
- 5. 断言不硬编码本机路径的 hash —— 用"多写法互相相等"+"与 `path.resolve` 语义一致"表达,保证跨机器可移植。
57
-
58
- **integration(可选强化)—— `test/coordinator-client.test.mjs`**:以 root 的一种写法 spawn/ensure,用另一种写法的客户端调用 → 成功(Windows 上是本 issue 的端到端复现;POSIX 上两写法本就是同一 socket)。
59
-
60
- ### 2.3 非目标
61
-
62
- - **大小写折叠**(`C:\...` vs `c:\...`):仍产生不同 hash。`path.resolve` 不改大小写;无条件 case-fold 会改变现有规范形式的 hash(破坏向后兼容),且 POSIX 大小写敏感语义不允许无条件折叠 → 记为已知限制。
63
- - POSIX 分支改动。
64
- - root 入口统一归一化(`defaultRoot()` 已归一化;runner argv 归一化无额外收益 → YAGNI)。
65
- - 不复用/改动 #114 的孤锁回收逻辑。
66
-
67
- ## 3. 验收矩阵
68
-
69
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
70
- |----|--------|----------|----------|----------|
71
- | A1 | win32 管道名对 root 写法不变 | 自动化验证(unit) | `node --test test/socket-path.test.mjs` | 4 种写法同一管道名 |
72
- | A2 | per-root 隔离保留 | 自动化验证(unit) | 同上 | 不同 root 管道名不同 |
73
- | A3 | 管道名格式/长度约束 | 自动化验证(unit) | 同上 | 前缀正确、16 hex、≤256 |
74
- | A4 | POSIX 行为不变 | 自动化验证(unit) | 同上 | 等于 `path.join(root, "coordinator.sock")` |
75
- | A5 | 端到端跨写法互通 | 自动化验证(integration,真机) | `node --test test/coordinator-client.test.mjs` | 一种写法启动、另一种写法 ensure 成功 |
76
- | A6 | 无回归 + 类型干净 | 自动化验证(static + unit) | `npm run typecheck`;`npm test` | typecheck 零错误;全量失败集不新增(Windows 既有环境类失败除外,需给出基线对比) |
77
-
78
- 无必须的用户实测项:A5 已覆盖原始症状的端到端路径(跨写法互通);如你希望,也可在合并后用正斜杠形式手动启一次 runner 复验。
79
-
80
- ## 4. 影响文件(预估)
81
-
82
- - `src/core/paths.mjs`(+~5 行:归一化 + 注释)
83
- - `test/socket-path.test.mjs`(+~40 行:不变量/隔离/长度/POSIX 断言)
84
- - `test/coordinator-client.test.mjs`(可选 +~30 行:跨写法 e2e)
@@ -1,92 +0,0 @@
1
- # Spec:Windows 上 coordinator 租约孤锁因 EPERM 无法自动接管(issue #114)
2
-
3
- 日期:2026-09-15 · 状态:approved(用户确认)
4
- 调研:issue 评论 R1(根因收敛 + Windows 测试基线)
5
- 前置:#112(PR #116)已修复「identity-less 超龄回收」;本 issue 修复其剩余阻塞
6
-
7
- ## 1. 根因(systematic-debugging Phase 1-3 结论)
8
-
9
- **唯一剩余根因**:`attemptAcquireLease`(src/core/locks.mjs)的 publish-rename 冲突白名单只认 `EEXIST`/`ENOTEMPTY`;Windows 上 `renameSync(candidate, lockPath)` 对已存在目标抛 `EPERM`(errno -4048,实测目标空/非空一致),被 catch 直接 `throw`,执行流永远到不了 `reclaimOrBlock`。
10
-
11
- **证据链**:
12
- 1. 实机(2026-09-12 / 09-13 两次):coordinator 暴毙 → 残留 `state-coordinator.lock` → `node runner/state-coordinator.mjs <root>` 输出 `lease unavailable (EPERM); ...; exiting`(exit 0)→ ensureCoordinator 10s 窗口内 probe 全败 → 面板报 `coordinator_unavailable`(DONE/archive/adopt 全断)。
13
- 2. 复现脚本(最新代码 a129b09,Windows):构造「死 pid + `startToken:null` + 超龄 10min」残留锁后 `tryAcquireOwnedViewLock` → `THREW: EPERM`(锁已满足 #112 全部回收条件,仍被白名单拒之门外)。
14
- 3. 代码静态核对:locks.mjs:224(renameSync)→ :228(`if (code !== "EEXIST" && code !== "ENOTEMPTY") throw err;`)。
15
- 4. Windows 测试基线:`node --test test/locks.test.mjs` = **20 tests / 4 fail**,4 个失败全部是 lease 接管路径(含 #112 新增的 2 个)→ 证明该路径在 Windows 从未真实执行过(CI 为 Linux;#112 的纯函数测试不触碰真实 rename)。
16
- 5. 影响面不止 coordinator:pty host 的 `host-meta` / `host-start` 租约共用 `attemptAcquireLease`,Windows 上锁已存在即同断。
17
-
18
- **已修复部分(#112,确认无需重做)**:`classifyLeaseOwner` 对 identity-less owner(含非 Linux `startToken:null`)在「`age >= ORPHAN_LEASE_AGE_MS`(5min) 且顶层 pid 确死」时返回 `reclaim`;活 pid 恒 `busy`;quarantine + inspectedToken 核对保留。
19
-
20
- ## 2. 修复设计
21
-
22
- ### 2.1 locks.mjs:rename 冲突码判定抽为纯函数并纳入 EPERM(D1)
23
-
24
- 新增导出纯函数:
25
-
26
- ```js
27
- /**
28
- * Whether a publish-rename failure means "the lock path already exists"
29
- * (contention) rather than a genuine filesystem error.
30
- * POSIX reports EEXIST/ENOTEMPTY; Windows reports EPERM (errno -4048) for
31
- * renaming a directory onto an existing directory — this op's platform
32
- * equivalent of EEXIST (issue #114).
33
- * @param {string|undefined} code
34
- * @returns {boolean}
35
- */
36
- export function isPublishConflictCode(code) {
37
- return code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM";
38
- }
39
- ```
40
-
41
- `attemptAcquireLease` catch 改为:
42
-
43
- ```js
44
- if (!isPublishConflictCode(code)) throw err;
45
- const verdict = reclaimOrBlock(lockPath, token, fs, isProcessDead, now);
46
- ```
47
-
48
- - **可测性拆分**:判定为纯函数(零副作用、可注入断言);副作用(reclaim/quarantine)仅在判定为冲突后触发,边界清晰。
49
- - **POSIX 语义变化**:真权限类 EPERM 将进入 `reclaimOrBlock` → 锁不存在/不可解析时返回 `blocked` → 最终抛 `LOCK_TIMEOUT`(原为 `EPERM`)。两者皆为失败退出、不产生错误回收,形态变化可接受(#48 的 `renameWithRetry` 亦已把 EPERM 列入无条件白名单,语义一致)。
50
- - **安全性**:能否回收仍由 `classifyLeaseOwner` 决定(pid 确死 / 超龄 + pid 确死),白名单化 EPERM 不放宽回收条件。
51
-
52
- ### 2.2 测试(D2)
53
-
54
- 1. **纯函数层**(unit):`isPublishConflictCode` 三分支断言(EEXIST/ENOTEMPTY/EPERM → true;ENOENT/EACCES/undefined → false)。
55
- 2. **接管路径层**(unit + 注入 fs 模拟 Windows):注入 `fs.renameSync` 首次对 `lockPath` 抛 EPERM,后续走真实 rename,覆盖:
56
- - 死 owner(identity 完整)→ `acquired=true`,lease 可正常 release;
57
- - 死 owner(identity-less 超龄)→ `acquired=true`(#112 路径打通);
58
- - 活 owner → `acquired=false, reason="busy"`(不误抢);
59
- - 身份不明(pid 活/无 token)→ 该场景回落 `blocked`/`busy` 既有语义。
60
- 3. **真机 integration**:Windows 上 `node --test test/locks.test.mjs` 全绿(修复前 16/20)。
61
-
62
- ### 2.3 非目标
63
-
64
- - 不实现 Windows `startToken`(PowerShell `CreationDate`):修复后最坏为 5min 超龄自愈(原为永久卡死),扩围留独立 issue;
65
- - 不改 `ORPHAN_LEASE_AGE_MS` / ensureCoordinator 的 `ENSURE_WINDOW_MS`;
66
- - 不动 host-meta 获取点 / service.mjs / pty-runner.mjs 的 identity 传递;
67
- - 不加 coordinator 失败诊断(#112 已在 store 侧落地 contended 诊断,coordinator 侧留后续)。
68
-
69
- ### 2.4 部署路径
70
-
71
- PR 合并 → 同步运行副本(`~/.pi/agent/git/github.com/zhuxixi/pi-agent-board`)→ 用户实测。合并前可用同一 worktree 代码做 U1。
72
-
73
- ## 3. 验收矩阵
74
-
75
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
76
- |----|--------|----------|----------|----------|
77
- | A1 | 冲突码判定含 EPERM | 自动化验证(unit) | `node --test test/locks.test.mjs`(新纯函数用例) | EEXIST/ENOTEMPTY/EPERM→true;其余→false |
78
- | A2 | EPERM 下死 owner(identity 完整)可接管 | 自动化验证(unit) | 同上(注入 fs renameSync 首抛 EPERM) | `acquired=true`,lease.release() 成功 |
79
- | A3 | EPERM 下死 owner(identity-less 超龄)可接管 | 自动化验证(unit) | 同上 | `acquired=true`(#112 回收路径被打通) |
80
- | A4 | EPERM 下活 owner 不误抢 | 自动化验证(unit) | 同上 | `acquired=false, reason="busy"` |
81
- | A5 | Windows 真机接管测试转绿 | 自动化验证(integration,真机) | `node --test test/locks.test.mjs`(Windows 11) | 20/20 pass(基线 16/20) |
82
- | A6 | 无回归 + 类型干净 | 自动化验证(static + unit) | `npm run typecheck`;`npm test` | typecheck 零错误;全量失败集不新增(Windows 既有环境类失败除外,需给出基线对比) |
83
- | A7 | 实机 coordinator 自愈 | 自动化验证(E2E 脚本,真机) | 临时 root:造超龄 identity-less 孤锁 → 启动 coordinator → 断言接管成功 + socket 可 probe | coordinator 常驻,owner.json 刷新为新 pid |
84
- | U1 | 面板实机回归(用户实测) | 用户实测 | 修复部署后:kill 当前 coordinator(保留孤锁)→ 等 5min 超龄 → 面板按 `d` → `y` | 标记 DONE 成功;无需手动删锁;coordinator 自拉起 |
85
-
86
- U1 执行时机说明:需 5min 等待窗口,可在修复部署后按用户时间安排;未执行前标记 `pending`,不宣称全量验收完成。
87
-
88
- ## 4. 影响文件(预估)
89
-
90
- - `src/core/locks.mjs`(+~12 行:导出纯函数 + catch 改造 + 注释)
91
- - `test/locks.test.mjs`(+~70 行:纯函数用例 + EPERM 注入接管用例)
92
- - 无其他生产文件改动。
@@ -1,123 +0,0 @@
1
- # Spec:← detach tier-1 锚点劫持 + editor-state reporter 端点断连(issue #103)
2
-
3
- 日期:2026-09-18 · 状态:approved(用户确认 D1 = R1)
4
- 基线:`main @ f657528`
5
- 调研:issue #103 评论 Round 1(根因复现)/ Round 2(方案约束)/ Round 3(f657528 复核 + 4 条增量发现)
6
-
7
- ## 1. 根因(systematic-debugging 结论)
8
-
9
- ### A. tier-1 反色锚点无形态校验
10
-
11
- `src/ui/pty-attach.ts:372 childInputLooksEmpty()` 调 `findLastInverseCellLine()`(自底向上取**第一个含反色 cell 的行**),`:381-386` 无条件把该行当编辑器行。
12
-
13
- 假设「反色 cell 只可能来自编辑器假光标」不成立:pi 本体在聊天区也渲染反色 —— `renderDiff` 的行内变更片段、通知条整条反色、searchCurrentMatch、alt-screen 选择高亮。而编辑器假光标行**常常不在 buffer 里**(差分帧不重绘未变化的行),于是锚点落到聊天区反色行 → 该行有文本 → 判"有草稿" → `←` 被吞。
14
-
15
- **间歇性来源**:假光标是否在 buffer 内取决于绘制时序。
16
-
17
- **证据**:view_06f9f09cee 的 screen.log 3.4MB 中 117 处 `ESC[7m`;活体 view_e81b4d8d10 的 2.7MB 日志中反色行 275 = 263 行「空白 + 单反色 cell」编辑器假光标 + 12 行聊天区 diff 内容。
18
-
19
- ### B. editor-state reporter 端点失配(仅 owned 路径)
20
-
21
- | 端 | 地址 |
22
- |---|---|
23
- | reporter 连接(`src/index.ts:118`) | `controlSocketPathFor()` → `views/<id>/control.sock`(posix)/ `\\.\pipe\pi-agent-board-<viewId>`(win32) |
24
- | owned runner 绑定(`runner/pty-runner.mjs:439` ← `src/runtime/service.mjs:279`) | `hostEndpointPathFor(..., instanceId)` → `control.<instanceId>.sock` / `…-<8hexhash>` |
25
- | legacy runner 绑定(`runner/pty-runner.mjs:108`) | `controlSocketPath()` —— 与 reporter 一致 |
26
-
27
- reporter 永远 ENOENT/EADDR → error 监听触发无限退避重连(上限 5s),永不成功 → `editorEmpty` 恒 `null` → 门禁永远落回有缺陷的启发式(A)。分派见 `runner/pty-runner.mjs:97-104`(有 `instanceId` 走 ownedMain)。
28
-
29
- **实测证据**:view_e81b4d8d10 目录内只有 `control.2cec1af1….sock`,无 `control.sock`;活得好的子 pi 进程 `/proc/<pid>/fd` 中 socket fd = 0。
30
-
31
- ### C. 直修 B 会掐死 warm-host 回收(阻塞级)
32
-
33
- reporter 是**常驻连接**,而 runner 把连接数直接当 `attachedClients`:
34
-
35
- - owned:`:704 clients.add(socket)` → `:502 attachedClients: clients.size`;`attachedEver` 在 hello 时翻(`:914`,probe 除外)
36
- - legacy:`:303 clients.add(socket)` + `:304 update({attachedEver: true})` —— **connect 即翻**
37
-
38
- 消费方 `src/core/warm-host-sweeper.mjs:53` 与 `src/runtime/service.mjs:807` 均以 `attachedClients !== 0` 跳过回收 → 宿主与子 session 永久泄漏(issue #75 的回收机制失效)。
39
-
40
- ## 2. 修复设计
41
-
42
- ### 2.1 A:tier-1 锚点形态校验(D1 = R1,已确认)
43
-
44
- 新增纯函数 `pickEditorAnchorLine(candidates)`,自底向上遍历**含反色字符的行**:只有「**恰好 1 个反色字符**」的候选才被采信。
45
-
46
- | 候选行形态(反色字符数 = 1) | 判定 | 依据 |
47
- |---|---|---|
48
- | 字形行(`isProbablyPiInputLine`) | 编辑器行,empty 由 `isProbablyEmptyPiInputLine` 决定 | 场景 B/B3 的既有语义(draft 保护保留) |
49
- | 非字形 + 文本全空白 | 新式 pi 空编辑器行(假光标)→ empty,放行 detach | pi-tui 空编辑器行 = 反色假光标 + 填充 |
50
- | 非字形 + 有文本 | 新式 pi draft 行 → **跳过**(R1 取舍:兜底不采信) | 见下方「已知代价」 |
51
- | 反色字符数 ≠ 1(多字符片段 / 通知条) | **跳过,继续向上扫** | 聊天区内容不得否决 detach |
52
- | 扫完全部候选 | `null` → 交 tier-2 字形扫描 → 仍无 → `return true`(放行) | #48/#69/#72 逃生链不变 |
53
-
54
- **为什么判据是「反色字符数」而不是「反色 cell 数」**(本机 `@xterm/headless` 6.x 实测):
55
-
56
- | 缓冲行 | 反色 cell 数 | 反色 `getChars().length` 累加 |
57
- |---|---|---|
58
- | `\x1b[7m草\x1b[27m稿`(宽字符 draft) | 2 | **1** |
59
- | `\x1b[7m \x1b[27m`(空编辑器假光标) | 1 | 1 |
60
- | `\x1b[7m Session saved \x1b[27m`(通知条) | 15 | 15 |
61
- | `+ 65 ## \x1b[7mR2 · \x1b[27m#\x1b[7m822 新 step\x1b[27m`(diff) | 16 | 15 |
62
-
63
- 宽字符占 2 个 cell 但只有 1 个字符(续格 `getChars()` 返回空串)。**按 cell 计数会把中文假光标算成 2 → 字形行被误跳过 → 场景 B 的 draft 保护被破坏**,故判据必须是累加 `getChars().length`。
64
-
65
- 判定所需的「反色字符数」超出 `findLastInverseCellLine()` 的存在性判断 → UI 侧新增提取(副作用隔离),判定逻辑全部移入纯函数。
66
-
67
- **已知代价(D1=R1 明确接受)**:新式 pi(编辑器行无 prompt 字形)+ 有 draft + reporter 不在线 → 兜底认不出 draft → 误 detach。可接受:detach 不丢草稿(子 session 继续跑,重新 attach 即回),而困死不可接受;B 修好后权威通道在位,A 只在连接窗口期/扩展缺失时兜底。按 K2 先例用场景 O3 钉住。
68
-
69
- ### 2.2 B:端点发现 + 反污染协议
70
-
71
- 1. **env 注入(两处 spawn 都做)**:`runner/pty-runner.mjs` 的 legacyMain(`:226`)与 ownedMain(`:784`)在子进程 env 中加 `AGENT_BOARD_CONTROL_SOCKET = socketPath`(各自"本宿主实际绑定的端点")。
72
- 2. **reporter 侧解析**:新增纯函数 `resolveControlEndpoint({ envSocketPath, platform, root, viewId })` → env 有值优先,缺失/空串回落 `controlSocketPathFor()`(兼容旧 runner 与 legacy)。
73
- 3. **身份握手**:reporter 每次(重)连成功后立刻发 `{ type: "hello", clientId: "editor-reporter" }`,随后才开始推 `editor_state`。
74
- 4. **runner 反污染**:hello 分类抽成纯函数 `classifyClientHello(msg)` → `"probe" | "editor-reporter" | "client"`:
75
- - `probe`:维持现状(`markProbe`,不写 `attachedEver`),但 owned/legacy 都要把 `attachedEver` 移到 hello 判定之后;
76
- - `editor-reporter`:把该 socket **移出 `clients`**(恢复 `attachedClients` 计数),不翻 `attachedEver`,转入独立的 `editorReporters` 集合;
77
- - `client`:现状(记 `attachedEver`)。
78
- 5. **生命周期补齐**:`editorReporters` 在宿主关闭阶梯(owned `:560-563`)中一并 destroy,避免悬挂 socket 与重连退避循环。
79
-
80
- ## 3. 验收矩阵
81
-
82
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
83
- |----|--------|----------|----------|----------|
84
- | A1 | 锚点判定纯函数 | 自动化(unit) | `node --test test/pty-input.test.mjs` | 覆盖:字形空行→empty、字形 draft(含中文宽字符)→non-empty、非字形空白单反色→empty、非字形 draft→跳过、多字符 diff 行→跳过、通知条→跳过、无候选/畸形输入→null |
85
- | A2 | 聊天区 diff 反色 + 无假光标空编辑器 → detach | 自动化(smoke) | `node --experimental-transform-types test-support/detach-gate-smoke.ts` 场景 **O1** | `detach=true`(当前 false) |
86
- | A3 | 反色通知条 + 同上 → detach | 自动化(smoke) | 同上,场景 **O2** | `detach=true`(当前 false) |
87
- | A4 | 钉住 D1/R1 取舍 | 自动化(smoke) | 同上,场景 **O3**:新式 draft(文本 + 单反色,无字形)+ `editorEmpty=null` | `detach=true`(有意放行,照 K2 先例钉住) |
88
- | A5 | 既有 detach 语义不回归 | 自动化(smoke) | 同上,A/B/B1/B2/B3/C/D/E/F/H/I/J/K1/K2/L/M/N/E2 | 全绿,B/B3 的 draft 保护保留 |
89
- | A6 | 端点解析纯函数 | 自动化(unit) | `node --test test/socket-path.test.mjs` | env 优先(posix 路径 / win32 pipe 名原样)、env 缺失或空 → 回落稳定路径 |
90
- | A7 | reporter 身份握手 | 自动化(unit) | `node --test test/editor-state-reporter.test.mjs` | 首次 connect 后首帧为 `hello{clientId:"editor-reporter"}`;重连后再次发送 |
91
- | A8 | reporter 不污染 attach 计数(owned) | 自动化(integration) | `node --test test/pty-runner.integration.test.mjs` | reporter 握手后 `attachedClients === 0` 且 `attachedEver === false`;真实客户端连接后 `attachedClients === 1` |
92
- | A9 | legacy 路径同 A8 | 自动化(integration) | 同上(legacy 分支) | connect 不再单独翻 `attachedEver`;reporter 连接零污染 |
93
- | A10 | 静态与全量 | 自动化(static + 全量 unit) | `npm run typecheck && npm test` | typecheck 0 error;测试 0 fail |
94
- | U1 | 实机:diff 高亮在场时空编辑器可退出 | 用户实测 | attach 一个 Running 会话,聊天区有 diff 高亮,编辑器清空后单按 `←`,重复 ≥5 次 | 每次都 detach,无需 Ctrl+← |
95
- | U2 | 实机:draft 保护仍在 | 用户实测 | attach 后输入草稿,按 `←` | 光标左移、**不** detach |
96
- | U3 | 实机:权威通道恢复 | 用户实测 | attach 后观察 hello / host.json(或临时诊断) | `editorEmpty` 为 `true/false` 而非恒 `null` |
97
- | U4 | 实机:warm-host 仍可回收 | 用户实测 | reporter 在线时让会话 idle,等 sweeper 周期 | 宿主被回收,无泄漏(回归 C 的阻塞点) |
98
-
99
- ## 4. 可测性拆分设计(实现硬约束)
100
-
101
- | 拆分单元 | 位置 | 形态 | 测试边界 |
102
- |---|---|---|---|
103
- | `pickEditorAnchorLine(candidates)` | `src/core/pty-input.mjs` | 纯函数:入参 `Array<{text, inverseCharCount}>`(自底向上),出参 `{empty:boolean}\|null` | 零 IO / 零终端依赖 → `test/pty-input.test.mjs` 直接单测 |
104
- | `resolveControlEndpoint({envSocketPath, platform, root, viewId})` | `src/core/paths.mjs` | 纯函数,platform 显式注入 | 零 IO;win32 分支断言 pipe 名原样返回,不真实绑定 |
105
- | `classifyClientHello(msg)` | `src/core/host-protocol.mjs`(新) | 纯函数 → `"probe"\|"editor-reporter"\|"client"` | 双端共用、零 IO → 单测 |
106
- | UI 侧提取 | `src/ui/pty-attach.ts` | 只做 `BufferLine → {text, inverseCharCount}`(副作用隔离) | 判定逻辑**不得**回流进 UI |
107
- | reporter 握手 | `src/core/editor-state-reporter.mjs` | 复用现有 DI(`connect`/`scheduler` 注入) | 用现有 manual-clock fakeScheduler 断言帧序 |
108
- | runner 反污染 | `runner/pty-runner.mjs` | `clients` / `editorReporters` 分账 | 走既有真实进程 integration 模式 |
109
-
110
- ## 5. 非目标
111
-
112
- - 不改 pi 本体渲染;不引入边框结构锚点 / 光标 cell 锚点方案(留作后续 issue)。
113
- - 不重构 warm-host 回收策略本身(本 issue 只保证它不被 reporter 掐死)。
114
- - 不动 `Ctrl+←` 兜底语义(#89)与断开态的 `←` 逃生(#48)。
115
-
116
- ## 6. 风险
117
-
118
- | 风险 | 对策 |
119
- |---|---|
120
- | R1 让新式 pi draft 失去兜底保护 | 已知代价,场景 O3 钉住;B 修好后权威在线率高 |
121
- | legacy `attachedEver` 语义调整波及别的消费方 | 实现前 grep 全部 `attachedEver` 消费点,逐条确认 |
122
- | Windows 命名管道名经 env 传递 | A6 单测(platform 注入)+ U1 真机;不依赖真实管道绑定 |
123
- | reporter 移出 `clients` 后漏清理 | A8/A9 断言 + 关闭阶梯测试(`editorReporters` 一并 destroy) |