@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.
- package/CHANGELOG.md +13 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +53 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +276 -0
- package/package.json +5 -11
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +14 -8
- package/IMPLEMENTATION_PLAN.md +0 -920
- package/PRD.md +0 -484
- package/PROGRESS.md +0 -142
- package/VERIFY.md +0 -115
- package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
- package/docs/EXPLORATION.md +0 -187
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
- package/docs/RELEASE.md +0 -148
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
- package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
- package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
- package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
- package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
- package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
- package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
- package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
- package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
- package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
- package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
- package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
- package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
- package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
- package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
- package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
- package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
- package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
- package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
- package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
- package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
- package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
- package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
- package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
- package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
- package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
- package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
- package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
- package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
- package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
- package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
- package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
- package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
- package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
- package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
- package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
- package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
- package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
- package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
- package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
- package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
- package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
- package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
- package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
- package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
- package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
- package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
- package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
- package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
- package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
- package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
- package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
- package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
- package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
- package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
- package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
- package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
- package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
- package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
- package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
- package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
- package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
- package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
- package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
- package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
- package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
- package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
- package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
- package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
- package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
- package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
- package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
- package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
- package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
- package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
- package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
- package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
- package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
- package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
- package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
- package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
- package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
- package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
- package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
- package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
- package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
- package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
- package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
- package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
- package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
- package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
- package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
- package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
- package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
- package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
- package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
- package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
- package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
- package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
- package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
- package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
- package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
- package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
- package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
- package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
- package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
- package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
- package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
- package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
- package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
- package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
- package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
- package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
- package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
- package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
- package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
- package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
- package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
- package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
- package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
- package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
- package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
- package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
- package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
- package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
- package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
- package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
- package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
- package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
- package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
- package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
- package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
- package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
- package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
- package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
- package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
- package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
- package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
- package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
- package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
- package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
- package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
- package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
- package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
- package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
- package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
- package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
- package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
- package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
- package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
- package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
- package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
- package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
- package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
- package/scripts/budget-audit.mjs +0 -135
- package/scripts/release.mjs +0 -446
- package/scripts/release_helper.mjs +0 -309
- package/scripts/run-perf-gate.mjs +0 -40
- package/scripts/run-tests.mjs +0 -375
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# issue #87 plan:legacy 死 host 安全回收实现
|
|
2
|
-
|
|
3
|
-
日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md
|
|
4
|
-
|
|
5
|
-
## 任务拆解
|
|
6
|
-
|
|
7
|
-
### T1:纯决策函数 `canFinalizeLegacyHost`(A1)
|
|
8
|
-
|
|
9
|
-
**文件**:`src/core/host-coordination.mjs`(新增导出,放 `classifyProbeResult` 附近)
|
|
10
|
-
|
|
11
|
-
```js
|
|
12
|
-
/**
|
|
13
|
-
* Whether a legacy (pre-instanceId) host record can be safely finalized as
|
|
14
|
-
* exited: the runner pid is provably dead (a reused pid reads alive →
|
|
15
|
-
* conservative false) AND the endpoint is unreachable. Satisfies spec §10.1's
|
|
16
|
-
* "never recovered" conservatism — recovery only fires when identity is
|
|
17
|
-
* certain (issue #87).
|
|
18
|
-
* @param {{ host: HostStatus|null, hostPid: number|null|undefined, hostPidAlive: boolean, probeClassification: string }} input
|
|
19
|
-
* @returns {boolean}
|
|
20
|
-
*/
|
|
21
|
-
export function canFinalizeLegacyHost({ host, hostPid, hostPidAlive, probeClassification }) {
|
|
22
|
-
if (!host || host.instanceId != null) return false;
|
|
23
|
-
if (host.state !== "starting" && host.state !== "alive") return false;
|
|
24
|
-
if (hostPid == null || hostPidAlive) return false;
|
|
25
|
-
return probeClassification === "missing" || probeClassification === "stale";
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
**测试**:`test/host-coordination.test.mjs` 新增真值表用例(在文件既有风格下):
|
|
30
|
-
- 全条件满足(starting/alive × missing/stale 四组合)→ true
|
|
31
|
-
- instanceId 非 null → false
|
|
32
|
-
- state ∈ {exited, failed, stopping} → false
|
|
33
|
-
- hostPid null / hostPidAlive true → false
|
|
34
|
-
- classification ∈ {ready, starting, occupied, unknown} → false
|
|
35
|
-
- host null → false
|
|
36
|
-
|
|
37
|
-
### T2:resolver 两处改造(A2-A5)
|
|
38
|
-
|
|
39
|
-
**文件**:`src/runtime/service.mjs`
|
|
40
|
-
|
|
41
|
-
1. **import 更新**:`readHostPid` 加入 store.mjs import 列表;`canFinalizeLegacyHost` 加入 host-coordination.mjs import(L28 现有 `canReplaceHost` 同处)。
|
|
42
|
-
|
|
43
|
-
2. **新增模块内 helper**(resolver 函数前,模块作用域):
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
/**
|
|
47
|
-
* Finalize a provably-dead legacy host as exited so the resolver loop's next
|
|
48
|
-
* iteration sees hostActive=false and claims a fresh new-protocol host
|
|
49
|
-
* (issue #87). Legacy records have no instanceId — no concurrent owner exists,
|
|
50
|
-
* so an unfenced writeHost is safe; the claim path is serialized by the
|
|
51
|
-
* host-start lease.
|
|
52
|
-
* @returns {boolean} true when finalized (caller should `continue` the loop).
|
|
53
|
-
*/
|
|
54
|
-
function finalizeDeadLegacyHost(root, viewId, host, probeClassification) {
|
|
55
|
-
const hostPid = Object.hasOwn(host, "runnerPid") ? host.runnerPid : readHostPid(root, viewId);
|
|
56
|
-
if (!canFinalizeLegacyHost({ host, hostPid, hostPidAlive: isAlive(hostPid), probeClassification })) return false;
|
|
57
|
-
writeHost(root, { ...host, state: "exited", endedAt: Date.now(), lastSeenAt: Date.now(), error: "legacy host finalized: runner pid dead (issue #87)" });
|
|
58
|
-
try {
|
|
59
|
-
appendDiagnostic(root, viewId, {
|
|
60
|
-
source: "service", level: "info", code: "legacy_host_finalized",
|
|
61
|
-
message: `Finalized stale legacy host (pid ${hostPid} dead, probe ${probeClassification}) — next attach claims a fresh host`,
|
|
62
|
-
details: { hostPid, probeClassification, previousState: host.state },
|
|
63
|
-
});
|
|
64
|
-
} catch { /* best effort */ }
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
注意:`writeHost` 签名是 `(root, host)`(host 内含 viewId),与 updateOwnedHost 不同。appendDiagnostic 签名参照文件内现有调用(`appendDiagnostic(root, viewId, {...})`,如 recoverHost 附近用法——实现时以现有调用为准)。
|
|
70
|
-
|
|
71
|
-
3. **resolver legacy alive 分支**(现 L963 附近):
|
|
72
|
-
|
|
73
|
-
```js
|
|
74
|
-
} else if (legacy) {
|
|
75
|
-
// Legacy host unreachable. spec §10.1 says never recover — unless the
|
|
76
|
-
// runner pid is provably dead, in which case finalize and self-heal
|
|
77
|
-
// (issue #87).
|
|
78
|
-
if (finalizeDeadLegacyHost(root, viewId, host, probe.classification)) {
|
|
79
|
-
await sleepFnImpl(HOST_PROBE_RETRY_MS);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
return pending(sessionFile, "legacy host unreachable — manual restart needed");
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
4. **resolver starting-legacy 分支**(现 L953-957 附近,`withinGrace || legacy` 等待分支):
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
if (withinGrace || legacy) {
|
|
90
|
-
// A legacy starting host whose runner is provably dead would wait out
|
|
91
|
-
// the grace window forever (withinGrace is always true for legacy) —
|
|
92
|
-
// finalize it now instead (issue #87).
|
|
93
|
-
if (legacy && finalizeDeadLegacyHost(root, viewId, host, probe.classification)) {
|
|
94
|
-
await sleepFnImpl(HOST_PROBE_RETRY_MS);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
// Normal cold start (or a legacy starting host — legacy is never recovered,
|
|
98
|
-
// spec §10.1): wait out the grace window.
|
|
99
|
-
await sleepFnImpl(HOST_PROBE_RETRY_MS);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
(此分支 probe 已在上方 L939 执行,classification 可得。)
|
|
105
|
-
|
|
106
|
-
**测试**:`test/host-resolver.test.mjs` 新增 4 个测试(fixtures 复用 hostRecord/scriptProbe/instantSleep/resolverService;launchHost override 模仿现有 adoption 测试的成功 spawn 模式):
|
|
107
|
-
|
|
108
|
-
- **A2 自愈闭环**:legacy alive host(hostRecord:instanceId:null、state:"alive"、**无 runnerPid 属性**——用 delete 或构造时排除)+ host-pid.json 写死 pid(writeHostPid 或 readHostPid 对应写法,参照 store.mjs 导出;用不可能存活的 pid 如 999999——注意须确认该 pid 在测试机不存在,现有 fixture aliveHost 已用 999999 表达"死 pid",同法)+ scriptProbe(["missing"]) + launchHost override 成功 spawn → 断言:resolveAttachTarget 最终返回 kind:"pty"(新 host);readHost 状态变迁——最终 host 是新 instance(launchHost 写入 starting);diagnostics 含 legacy_host_finalized。
|
|
109
|
-
- 实现细节:claim/launch 后 readHost 读到的是新 host 记录,验证 finalize 发生要靠 diagnostics 断言 + resolve 结果非 pending。
|
|
110
|
-
- **A3 保守分支**:同上但 host-pid.json 写 `process.pid`(活 pid)→ 断言返回 pending(reason 含 "legacy host unreachable")且 host.json 未被改写(state 仍 alive)。
|
|
111
|
-
- **A4 starting-legacy 回收**:legacy starting host(claimAt 久远超 grace,pid 死)+ probe missing + launchHost spawn 成功 → 断言不自 deadline timeout、最终 kind:"pty"、diagnostics 含 legacy_host_finalized。
|
|
112
|
-
- **A5 unknown 不回收**:pid 死 + scriptProbe(["unknown"]) → 断言 pending 且不写盘。
|
|
113
|
-
|
|
114
|
-
⚠️ fixture 要点:hostRecord 默认写 `runnerPid: null`——这会让 `Object.hasOwn(host, "runnerPid")` 为 true,fallback 不到 host-pid.json。legacy fixture 必须构造**没有 runnerPid 属性**的记录(hostRecord 加 `legacy: true` 选项删除 instanceId/runnerPid 等字段,或新写 `legacyHostRecord` helper),并配 `writeHostPid(root, viewId, pid)` 写镜像。
|
|
115
|
-
|
|
116
|
-
### T3:全量回归(A6)
|
|
117
|
-
|
|
118
|
-
`npm test` + `npm run typecheck`(基线 602)。
|
|
119
|
-
|
|
120
|
-
## 验收对账
|
|
121
|
-
|
|
122
|
-
- A1 → T1 真值表单测
|
|
123
|
-
- A2/A3/A4/A5 → T2 四个集成测试
|
|
124
|
-
- A6 → T3
|
|
125
|
-
- U1(用户实测,合并前):本机 6 个真实卡死 legacy view attach 验证——主 session 合并前提示用户执行,或合并后实测反馈。
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# issue #86 plan:spawn 异步 error 兜底实现
|
|
2
|
-
|
|
3
|
-
日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md
|
|
4
|
-
|
|
5
|
-
## 任务拆解
|
|
6
|
-
|
|
7
|
-
### T1:launch.mjs spawnDetached helper + 4 处收敛(A1, A2)
|
|
8
|
-
|
|
9
|
-
**改动**(`src/core/launch.mjs`):
|
|
10
|
-
1. 文件底部(或 import 后)新增模块内 helper:
|
|
11
|
-
```js
|
|
12
|
-
function spawnDetached(command, args, cwd) {
|
|
13
|
-
const child = spawn(command, args, {
|
|
14
|
-
cwd,
|
|
15
|
-
detached: true,
|
|
16
|
-
stdio: "ignore",
|
|
17
|
-
env: process.env,
|
|
18
|
-
// Windows: detached children get their own console window unless
|
|
19
|
-
// suppressed (CREATE_NO_WINDOW; no-op on POSIX) — issue #49.
|
|
20
|
-
windowsHide: true,
|
|
21
|
-
});
|
|
22
|
-
// Swallow async spawn failures (e.g. transient ENOENT on the node binary):
|
|
23
|
-
// without an 'error' listener the EventEmitter rethrows as uncaughtException
|
|
24
|
-
// and takes down the whole host Pi process (issue #86). Callers already
|
|
25
|
-
// record state "failed" via the pid == null branch.
|
|
26
|
-
child.on("error", () => {});
|
|
27
|
-
child.unref();
|
|
28
|
-
return child;
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
2. `launchRun` / `launchHost` / `launchTitle` / `launchAutoState` 四处的 `spawn(...)` + `child.unref()` 收敛为 `const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);`,返回值逻辑(`child.pid ?? null`、writePid 等)不变。
|
|
32
|
-
|
|
33
|
-
**测试**(`test/launch.test.mjs`,复用现有注入模式):
|
|
34
|
-
- 新增 1 个测试:注入 `node: "/nonexistent/node-ENOENT-test"`,依次调用 4 个入口,断言:各自正常返回 `{ pid: null }`;`process.on("uncaughtException")` spy 不被触发;等待 ~300ms 让异步 error 有机会冒泡(进程崩 = 测试天然失败)。
|
|
35
|
-
- 现有 4 个测试不动(A2 回归)。
|
|
36
|
-
|
|
37
|
-
### T2:pty-attach.ts openExternalTarget error 兜底(A3)
|
|
38
|
-
|
|
39
|
-
**改动**(`src/ui/pty-attach.ts` L1150-1167):三分支统一为:
|
|
40
|
-
```ts
|
|
41
|
-
const child = spawn("xdg-open", [sanitized], { detached: true, stdio: "ignore" });
|
|
42
|
-
child.on("error", () => {});
|
|
43
|
-
child.unref();
|
|
44
|
-
```
|
|
45
|
-
(darwin `open`、win32 `cmd /c start` 同理)。参照 L847-848 xclip 既有模式。
|
|
46
|
-
|
|
47
|
-
### T3:全量回归(A4)
|
|
48
|
-
|
|
49
|
-
`npm test` + `npm run typecheck`,568+ 全绿。
|
|
50
|
-
|
|
51
|
-
## 验收对账
|
|
52
|
-
|
|
53
|
-
- A1 → T1 新测试
|
|
54
|
-
- A2 → T1 既有测试回归
|
|
55
|
-
- A3 → T2 代码审查 + `rg 'child.on\("error"' src/ui/pty-attach.ts` 断言三分支(A3 static)
|
|
56
|
-
- A4 → T3
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# issue #90 plan:defaultModel 失效防护实现
|
|
2
|
-
|
|
3
|
-
日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md
|
|
4
|
-
|
|
5
|
-
## 任务拆解
|
|
6
|
-
|
|
7
|
-
### T1:core 匹配函数 + service 注入(A1, A4)
|
|
8
|
-
|
|
9
|
-
1. `src/core/launch-options.mjs` 新增导出(spec D1.1 签名原样):
|
|
10
|
-
```js
|
|
11
|
-
export function modelRefAvailable(modelRef, availableModels) {
|
|
12
|
-
const ref = String(modelRef ?? "").trim().toLowerCase();
|
|
13
|
-
if (!ref) return true; // no constraint
|
|
14
|
-
if (!availableModels || availableModels.length === 0) return true; // can't judge → allow
|
|
15
|
-
return availableModels.some((m) => `${m.provider}/${m.id}`.toLowerCase() === ref);
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
2. 单测:若已有 `test/launch-options*.test.mjs` 则加入,否则新建。真值表:精确匹配(大小写混合)→ true;不匹配 → false;null/空串 → true;availableModels undefined/[] → true;部分匹配("glm-5.3" 不配 provider)→ false。
|
|
19
|
-
3. `src/ui/dashboard.ts` L1738 `findLaunchModelByRef` 改为内部调用 core 的匹配逻辑(保持返回 LaunchModel 对象:find 复用 `modelRefAvailable` 不可行——它返回 boolean;改为直接复用 core 里新增的共享比较,或最简单:dashboard 本地函数改为 `models.find(m => modelRefAvailable(ref, [m])) ?? null`。选最简单且单一事实源的写法)。
|
|
20
|
-
- 注:若这步让 diff 变脏(import 路径等),可以跳过,dashboard 本地函数保持——在 plan 偏差里说明即可。优先级低。
|
|
21
|
-
|
|
22
|
-
### T2:service 校验接线(A2, A3, A5)
|
|
23
|
-
|
|
24
|
-
1. `createService` opts 新增:`availableModels`(`() => Array<{provider:string,id:string}> | undefined`;默认 undefined → 跳过校验)。JSDoc typedef 更新(文件内 createService opts 注释处)。
|
|
25
|
-
2. service.mjs 模块内新增 helper:
|
|
26
|
-
```js
|
|
27
|
-
/** @returns {string|null} error message when the view's defaultModel is provably unavailable. */
|
|
28
|
-
function validateViewModelMeta(meta) {
|
|
29
|
-
const model = meta.defaultModel ?? null;
|
|
30
|
-
if (!model || !optsAvailableModels) return null;
|
|
31
|
-
const list = optsAvailableModels() ?? undefined;
|
|
32
|
-
if (modelRefAvailable(model, list)) return null;
|
|
33
|
-
return `Model "${model}" configured for this session is no longer available — update the view's model (or clear defaultModel) and retry attach.`;
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
(注意 availableModels() 调用本身 try/catch → undefined。)
|
|
37
|
-
3. `startHostUnderLease`(L205):在 canReplaceHost 检查之后、instanceId 生成之前插入:
|
|
38
|
-
```js
|
|
39
|
-
const modelError = validateViewModelMeta(meta);
|
|
40
|
-
if (modelError) return { ok: false, error: modelError };
|
|
41
|
-
```
|
|
42
|
-
(校验失败不留 claim、不 spawn。)
|
|
43
|
-
4. `adoptClaimedHost`(L775 起):config 构建前插入校验;失败时:
|
|
44
|
-
```js
|
|
45
|
-
updateOwnedHost(root, viewId, instanceId, (h) => ({ ...h, state: "failed", endedAt: nowImpl(), exitCode: 1, error: modelError, claimPid: null, claimIdentity: null }));
|
|
46
|
-
return { ok: true, pending: true, socketPath: null, instanceId };
|
|
47
|
-
```
|
|
48
|
-
(对齐该函数既有 spawnError 失败处理模式。)
|
|
49
|
-
5. `src/index.ts` serviceFor 与 `src/commands/agent-board.ts` flag 路径注入:
|
|
50
|
-
```ts
|
|
51
|
-
availableModels: () => { try { return ctx.modelRegistry.getAvailable(); } catch { return undefined; } },
|
|
52
|
-
```
|
|
53
|
-
(agent-board.ts flag 路径 L115 的 createService 调用同款注入;dashboard 路径 L95 已有 availableModels 变量但那是 UI deps,service 注入仍需单独传。)
|
|
54
|
-
6. 集成测试(`test/host-resolver.test.mjs` 或 `test/service.test.mjs`,看 ensure 测试在哪更顺):
|
|
55
|
-
- A2:view meta defaultModel="glm/glm-5.3",注入 availableModels: () => [{provider:"zai-coding-cn",id:"glm-5.3"}],launchHost spy → ensureHost("v1") 断言 ok:false、error 含模型名、spy 零调用、readHost 无 starting 残留;
|
|
56
|
-
- A3 对照:availableModels 含 glm/glm-5.3 → 正常 claim+spawn(断言 started/started pending 路径与现状一致);
|
|
57
|
-
- A4:不注入 availableModels → 跳过校验正常 launch;
|
|
58
|
-
- A5:废弃 starting claim(adopt 路径,参照现有 adoption 测试 fixture)+ 失效模型 → 断言落 failed、不 spawn。
|
|
59
|
-
|
|
60
|
-
### T3:runner exit 归因(A6, A7)
|
|
61
|
-
|
|
62
|
-
1. `src/core/heuristics.mjs` 新增导出:
|
|
63
|
-
```js
|
|
64
|
-
/**
|
|
65
|
-
* Last non-empty visible line of a raw terminal log chunk (ANSI/OSC stripped,
|
|
66
|
-
* per-line carriage-return resolved). @returns {string|null}
|
|
67
|
-
*/
|
|
68
|
-
export function lastVisibleLogLine(text, maxLen = 200)
|
|
69
|
-
```
|
|
70
|
-
实现:strip `/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g`(OSC)→ `/\x1b\[[0-9;?]*[ -/]*[@-~]/g`(CSI)→ 其他 `\x1b.` 单字符转义 → 按 `\n` 切分 → 每行取最后一个 `\r` 之后段 → trim → 过滤空 → 取最后一行 → truncate(maxLen)(复用同文件 truncate)。
|
|
71
|
-
2. 单测(heuristics 测试文件):ANSI 颜色、OSC 链接、`\r` 覆盖行、空行跳过、截断、空输入 → null。
|
|
72
|
-
3. `runner/pty-runner.mjs` child exit 回调(L213-223 区域):
|
|
73
|
-
```js
|
|
74
|
-
if (!crashed) {
|
|
75
|
-
let exitError = null;
|
|
76
|
-
if (exitCode !== 0) {
|
|
77
|
-
try {
|
|
78
|
-
const tail = readScreenLogTail(screenLog, 8192); // openSync/readSync seek 尾部
|
|
79
|
-
exitError = lastVisibleLogLine(tail);
|
|
80
|
-
} catch { /* best effort */ }
|
|
81
|
-
}
|
|
82
|
-
update({ state: "exited", endedAt: Date.now(), exitCode, childPid: null, ...(exitError ? { error: exitError } : {}) });
|
|
83
|
-
...
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
(screenLog 变量在 L62 作用域内可见;确认该回调能访问到。readScreenLogTail 写成 runner 内私有小函数。)
|
|
87
|
-
4. 集成测试(test/pty-runner.integration.test.mjs):用 test-support/ 下 fake pi 模式(看 fake-slow-start-pi.mjs;若无"立即 exit 1 + 输出错误"的 fake 就新增一个 `fake-failing-pi.mjs`:打印 `Error: Model "glm/glm-5.3" not found.` 后 exit 1)→ spawn runner → 等 exit → 断言 readHost 的 error 含 "Model" / "not found";对照 exit 0(现有 fake)→ error 字段保持 null。注意 hasNodePty skip 守卫参照现有测试。
|
|
88
|
-
|
|
89
|
-
### T4:全量回归(A8)
|
|
90
|
-
|
|
91
|
-
`npm test` + `npm run typecheck`(基线 608)。
|
|
92
|
-
|
|
93
|
-
## 验收对账
|
|
94
|
-
|
|
95
|
-
- A1 → T1.2 · A2/A3/A4/A5 → T2.6 · A6 → T3.2 · A7 → T3.4 · A8 → T4
|
|
96
|
-
- U1(用户实测,合并后):view_539a5e9e20 恢复失效 defaultModel → attach 看 notify + diagnostics 不再累积;改回有效模型 attach 成功。
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
# claimPid-Blocks-Replace Fix Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Fix issue #99 — an exited/failed host whose `claimPid` (the dashboard process) is still alive must be replaceable, so attach no longer pends to `host start timed out`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** The fix relaxes one pure decision function (`canReplaceHost` in `src/core/host-coordination.mjs`): the claim role no longer participates in the replacement gate for terminal hosts, because claim protection (a launcher mid-transaction between claim and spawn) only matters while a host is `starting`. The observation helper (`observeHostForReplace` in `src/runtime/service.mjs`) drops its now-unused `claimObservation` field.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node.js (node:test, node:assert/strict), plain ESM modules, no new dependencies.
|
|
10
|
-
|
|
11
|
-
**Spec:** `docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md`
|
|
12
|
-
|
|
13
|
-
## Global Constraints
|
|
14
|
-
|
|
15
|
-
- Change only the claim-role gate semantics; runner/child unknown observations must still block replacement (spec A2), `launchLeaseActive` must still block (spec A3), non-terminal hosts must still refuse (spec A4).
|
|
16
|
-
- `canReplaceHost` signature loses `claimObservation`; `observeHostForReplace` stops computing it (one fewer `process.kill(pid, 0)` syscall).
|
|
17
|
-
- All existing tests in `test/host-coordination.test.mjs` and `test/host-resolver.test.mjs` must keep passing (no behavioral regressions outside the claim-role gate).
|
|
18
|
-
- `npm run typecheck` must pass (service.mjs is .mjs but the repo runs tsc over TS sources; keep JSDoc types consistent).
|
|
19
|
-
- No changes to pty-runner.mjs, store.mjs, or host.json structure (spec non-goals).
|
|
20
|
-
|
|
21
|
-
## Acceptance traceability
|
|
22
|
-
|
|
23
|
-
| Spec ID | Plan coverage |
|
|
24
|
-
|---------|---------------|
|
|
25
|
-
| A1 (exited/failed + live claimPid → replaceable) | Task 1 Step 1 test `canReplaceHost allows replacing a terminal host whose claimPid is still alive` |
|
|
26
|
-
| A2 (runner/child unknown still blocks) | Task 1 Step 1 test `canReplaceHost still refuses unknown runner/child observations` + legacy case 1 of `canReplaceHost refuses unknown observations` |
|
|
27
|
-
| A3 (launchLeaseActive still blocks) | Task 1 Step 1 legacy case 3 of `canReplaceHost refuses unknown observations` |
|
|
28
|
-
| A4 (non-terminal hosts refuse; host null) | Task 1 Step 1 test `canReplaceHost refuses non-terminal hosts and null host` |
|
|
29
|
-
| A5 (foreign/not_started releasable) | Task 1 Step 1 test `canReplaceHost SAFE_TO_RELEASE boundary regression` |
|
|
30
|
-
| A6 (resolver integration: exited + live claimPid → new host) | Task 2 |
|
|
31
|
-
| U1 (real dashboard attach) | Post-implementation manual task (Task 3) |
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
### Task 1: Relax `canReplaceHost` claim-role gate + drop `claimObservation` (A1-A5)
|
|
36
|
-
|
|
37
|
-
**Files:**
|
|
38
|
-
- Modify: `src/core/host-coordination.mjs:60-80` (canReplaceHost + its JSDoc)
|
|
39
|
-
- Modify: `src/runtime/service.mjs:2006-2013` (observeHostForReplace)
|
|
40
|
-
- Test: `test/host-coordination.test.mjs:30-35`
|
|
41
|
-
|
|
42
|
-
**Interfaces:**
|
|
43
|
-
- Consumes: `SAFE_TO_RELEASE` set (already defined at `host-coordination.mjs:57`).
|
|
44
|
-
- Produces: `canReplaceHost({ host, runnerObservation, childObservation, launchLeaseActive }) → boolean` — the `claimObservation` parameter is REMOVED. Sole caller `observeHostForReplace` (service.mjs, internal, not exported) stops passing it. Task 2's integration test relies on this behavior only indirectly (through `resolveAttachTarget`), so no signature dependency.
|
|
45
|
-
|
|
46
|
-
- [ ] **Step 1: Write the failing tests**
|
|
47
|
-
|
|
48
|
-
In `test/host-coordination.test.mjs`, first EDIT the existing `canReplaceHost refuses unknown observations` test (line 30-35) to drop the `claimObservation` argument from all three assertions (the parameter is being removed):
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
test("canReplaceHost refuses unknown observations", () => {
|
|
52
|
-
const host = { state: "failed" };
|
|
53
|
-
assert.equal(canReplaceHost({ host, runnerObservation: "unknown", childObservation: "dead", launchLeaseActive: false }), false);
|
|
54
|
-
assert.equal(canReplaceHost({ host, runnerObservation: "dead", childObservation: "not_started", launchLeaseActive: false }), true);
|
|
55
|
-
assert.equal(canReplaceHost({ host, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: true }), false);
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Then ADD these four tests right after it:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
test("canReplaceHost allows replacing a terminal host whose claimPid is still alive (issue #99)", () => {
|
|
63
|
-
// The bug: an exited/failed host keeps its claimPid (the dashboard process
|
|
64
|
-
// that wrote the claim), and a live pid observed as "unknown" used to block
|
|
65
|
-
// replacement forever — attach pended to "host start timed out". Claim
|
|
66
|
-
// protection only matters while a claim is mid-transaction (state
|
|
67
|
-
// "starting"); a terminal host cannot still be being launched.
|
|
68
|
-
assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), true);
|
|
69
|
-
assert.equal(canReplaceHost({ host: { state: "failed" }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("canReplaceHost still refuses unknown runner/child observations (issue #99 conservatism)", () => {
|
|
73
|
-
assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "unknown", childObservation: "dead", launchLeaseActive: false }), false);
|
|
74
|
-
assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "dead", childObservation: "unknown", launchLeaseActive: false }), false);
|
|
75
|
-
assert.equal(canReplaceHost({ host: { state: "failed" }, runnerObservation: "unknown", childObservation: "unknown", launchLeaseActive: false }), false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("canReplaceHost refuses non-terminal hosts and null host (issue #99)", () => {
|
|
79
|
-
for (const state of ["starting", "alive", "stopping"]) {
|
|
80
|
-
assert.equal(canReplaceHost({ host: { state }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), false, `state ${state} must refuse`);
|
|
81
|
-
}
|
|
82
|
-
assert.equal(canReplaceHost({ host: null, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("canReplaceHost SAFE_TO_RELEASE boundary regression (issue #99)", () => {
|
|
86
|
-
assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "foreign", childObservation: "dead", launchLeaseActive: false }), true, "foreign runner (pid reuse) is releasable");
|
|
87
|
-
assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "not_started", childObservation: "not_started", launchLeaseActive: false }), true);
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
92
|
-
|
|
93
|
-
Run: `node --test test/host-coordination.test.mjs 2>&1 | tail -30`
|
|
94
|
-
Expected: FAIL — the first new test fails (`exited` + dead runner/child returns `false` under the old three-role gate). The edited legacy test fails too: the old implementation ignores the removed `claimObservation` key but still requires `claimObservation` in SAFE_TO_RELEASE via `undefined → not in set → false`… actually with `claimObservation` absent, `SAFE_TO_RELEASE.has(undefined)` is `false`, so case 2 of the legacy test (`"dead"` args → expected `true`) FAILS under the old code. Both failures prove the tests exercise the gate.
|
|
95
|
-
|
|
96
|
-
- [ ] **Step 3: Implement the relaxation**
|
|
97
|
-
|
|
98
|
-
In `src/core/host-coordination.mjs`, replace the `canReplaceHost` function (lines ~63-80) with:
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
/**
|
|
102
|
-
* Whether an exited/failed host can be replaced by a new claim. The runner and
|
|
103
|
-
* child roles must be provably gone; any `unknown` observation or an active
|
|
104
|
-
* launch lease blocks replacement. The claim role does NOT participate: claim
|
|
105
|
-
* protection (a launcher mid-transaction between claim and spawn) only matters
|
|
106
|
-
* while the host is `starting`, and this gate only ever sees terminal hosts —
|
|
107
|
-
* a terminal host cannot still be being launched (issue #99: a live claimPid —
|
|
108
|
-
* the dashboard process that wrote the claim — must not block re-attach).
|
|
109
|
-
* @param {{
|
|
110
|
-
* host: HostStatus|null|undefined,
|
|
111
|
-
* runnerObservation: string,
|
|
112
|
-
* childObservation: string,
|
|
113
|
-
* launchLeaseActive: boolean,
|
|
114
|
-
* }} input
|
|
115
|
-
* @returns {boolean}
|
|
116
|
-
*/
|
|
117
|
-
export function canReplaceHost({ host, runnerObservation, childObservation, launchLeaseActive }) {
|
|
118
|
-
if (!host || (host.state !== "exited" && host.state !== "failed")) return false;
|
|
119
|
-
if (launchLeaseActive) return false;
|
|
120
|
-
return (
|
|
121
|
-
SAFE_TO_RELEASE.has(runnerObservation) &&
|
|
122
|
-
SAFE_TO_RELEASE.has(childObservation)
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
In `src/runtime/service.mjs`, edit `observeHostForReplace` (line ~2006) to drop the `claimObservation` line:
|
|
128
|
-
|
|
129
|
-
```js
|
|
130
|
-
/** @param {import("../core/types.mjs").HostStatus|null} host */
|
|
131
|
-
function observeHostForReplace(host) {
|
|
132
|
-
return {
|
|
133
|
-
host,
|
|
134
|
-
runnerObservation: conservativeObservation(host?.runnerPid ?? null),
|
|
135
|
-
childObservation: conservativeObservation(host?.childPid ?? null),
|
|
136
|
-
launchLeaseActive: false,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
- [ ] **Step 4: Run tests to verify they pass**
|
|
142
|
-
|
|
143
|
-
Run: `node --test test/host-coordination.test.mjs 2>&1 | tail -10`
|
|
144
|
-
Expected: PASS — all tests in the file pass (4 new + edited legacy + all untouched).
|
|
145
|
-
|
|
146
|
-
Run: `node --test test/host-resolver.test.mjs test/host-recovery.test.mjs test/host-crash.test.mjs 2>&1 | tail -10`
|
|
147
|
-
Expected: PASS — no regressions in adjacent host suites.
|
|
148
|
-
|
|
149
|
-
Run: `npm run typecheck`
|
|
150
|
-
Expected: exit 0.
|
|
151
|
-
|
|
152
|
-
- [ ] **Step 5: Commit**
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
git add src/core/host-coordination.mjs src/runtime/service.mjs test/host-coordination.test.mjs
|
|
156
|
-
git commit -m "fix(host): claim role no longer blocks terminal host replacement (#99)"
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
### Task 2: Resolver integration test — exited host + live claimPid attaches via fresh spawn (A6)
|
|
162
|
-
|
|
163
|
-
**Files:**
|
|
164
|
-
- Test: `test/host-resolver.test.mjs` (add one test after the `resolver finalizes a provably-dead legacy alive host` test, ~line 163)
|
|
165
|
-
|
|
166
|
-
**Interfaces:**
|
|
167
|
-
- Consumes: existing helpers `freshRoot`, `resolverService`, `healServiceOverrides(probe, spawns)`, `scriptProbe(seq)`, `hostRecord(root, viewId, over)` (defaults `claimPid: process.pid` — exactly the live-claimer shape), `createView` (returns `{ sessionFile, ... }`), and `writeFileSync` (already imported). Task 1's relaxed `canReplaceHost` must be in place — this test verifies the full attach chain (resolver → ensureHost → startHostUnderLease → canReplaceHost → spawn → probe ready).
|
|
168
|
-
- Produces: nothing downstream (terminal verification task).
|
|
169
|
-
|
|
170
|
-
- [ ] **Step 1: Write the integration test**
|
|
171
|
-
|
|
172
|
-
Add to `test/host-resolver.test.mjs` after the issue #87 legacy-alive test (~line 163):
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
test("resolver replaces an exited host whose claimPid is still alive (issue #99)", async () => {
|
|
176
|
-
const root = freshRoot();
|
|
177
|
-
try {
|
|
178
|
-
const meta = createView(root, { id: "v1", name: "a", cwd: "/r" });
|
|
179
|
-
writeFileSync(meta.sessionFile, "");
|
|
180
|
-
// The bug's exact shape: the host ran to completion (exited, exitCode 0,
|
|
181
|
-
// stopReason child_exit) but its claimPid — the dashboard process that
|
|
182
|
-
// wrote the claim — is STILL ALIVE (hostRecord defaults claimPid to
|
|
183
|
-
// process.pid). Before the fix, canReplaceHost saw the live claim as
|
|
184
|
-
// "unknown" and the resolver pended to "host start timed out".
|
|
185
|
-
hostRecord(root, "v1", {
|
|
186
|
-
instanceId: "i1",
|
|
187
|
-
state: "exited",
|
|
188
|
-
runnerPid: 999999,
|
|
189
|
-
childPid: null,
|
|
190
|
-
endedAt: Date.now(),
|
|
191
|
-
exitCode: 0,
|
|
192
|
-
stopReason: "child_exit",
|
|
193
|
-
});
|
|
194
|
-
const probe = scriptProbe(["ready"]);
|
|
195
|
-
const spawns = [];
|
|
196
|
-
const svc = resolverService(root, healServiceOverrides(probe, spawns));
|
|
197
|
-
const result = await svc.resolveAttachTarget("v1", { timeoutMs: 2_000 });
|
|
198
|
-
assert.equal(result.kind, "pty", `must replace the exited host despite the live claimPid: ${JSON.stringify(result)}`);
|
|
199
|
-
assert.equal(spawns.length, 1, "exactly one fresh claim spawn");
|
|
200
|
-
assert.notEqual(result.instanceId, "i1", "attaches to the replacement instance");
|
|
201
|
-
} finally {
|
|
202
|
-
rmSync(root, { recursive: true, force: true });
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
- [ ] **Step 2: Sanity-verify the test fails against the pre-fix gate (optional but recommended)**
|
|
208
|
-
|
|
209
|
-
Temporarily `git stash` the Task 1 commit (`git stash` won't work across commits — instead: `git checkout HEAD~1 -- src/core/host-coordination.mjs src/runtime/service.mjs`), then run:
|
|
210
|
-
|
|
211
|
-
Run: `node --test --test-name-pattern "issue #99" test/host-resolver.test.mjs 2>&1 | tail -15`
|
|
212
|
-
Expected: the new test FAILS or times out (resolver pends — the pre-fix behavior). Then restore: `git checkout HEAD -- src/core/host-coordination.mjs src/runtime/service.mjs`.
|
|
213
|
-
|
|
214
|
-
If the timeout makes the run slow, the 2_000 ms timeoutMs bounds it.
|
|
215
|
-
|
|
216
|
-
- [ ] **Step 3: Run the test against the fix**
|
|
217
|
-
|
|
218
|
-
Run: `node --test --test-name-pattern "issue #99" test/host-resolver.test.mjs 2>&1 | tail -10`
|
|
219
|
-
Expected: PASS — `kind: "pty"`, exactly one spawn, replacement instanceId.
|
|
220
|
-
|
|
221
|
-
- [ ] **Step 4: Run the full suite**
|
|
222
|
-
|
|
223
|
-
Run: `npm test 2>&1 | tail -15`
|
|
224
|
-
Expected: PASS — all suites green, no regressions.
|
|
225
|
-
|
|
226
|
-
Run: `npm run typecheck`
|
|
227
|
-
Expected: exit 0.
|
|
228
|
-
|
|
229
|
-
- [ ] **Step 5: Commit**
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
git add test/host-resolver.test.mjs
|
|
233
|
-
git commit -m "test(resolver): exited host with live claimPid attaches via fresh spawn (#99)"
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
### Task 3: U1 manual verification (post-implementation, user-executed)
|
|
239
|
-
|
|
240
|
-
**Files:** none (manual).
|
|
241
|
-
|
|
242
|
-
**Interfaces:** none.
|
|
243
|
-
|
|
244
|
-
- [ ] **Step 1: Restart dashboard process** (loads new code — the "immediately effective on existing bad records" property requires restart).
|
|
245
|
-
|
|
246
|
-
- [ ] **Step 2: Attach view_2472d82627 from the dashboard** — observe: attach enters the session, history renders, no `host start timed out`.
|
|
247
|
-
|
|
248
|
-
- [ ] **Step 3: Exit the session, attach again** — confirm repeatability.
|
|
249
|
-
|
|
250
|
-
- [ ] **Step 4: Same check on view_4b667ad75d / view_c038badb30** (the other two exited + live-claimPid views).
|
|
251
|
-
|
|
252
|
-
- [ ] **Step 5: Record results in the issue** (comment each view's outcome; mark U1 pass/pending in the final report).
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Self-Review
|
|
257
|
-
|
|
258
|
-
**1. Spec coverage:**
|
|
259
|
-
- A1-A5 → Task 1 Step 1 (three new tests + edited legacy test covers A2/A3 cases) ✓
|
|
260
|
-
- A6 → Task 2 ✓
|
|
261
|
-
- U1 → Task 3 ✓
|
|
262
|
-
- 改动文件清单 (spec) → Task 1 + Task 2 files match exactly (host-coordination.mjs, service.mjs, host-coordination.test.mjs, host-resolver.test.mjs) ✓
|
|
263
|
-
- 非目标: no pty-runner/store/host.json changes in any task ✓
|
|
264
|
-
|
|
265
|
-
**2. Placeholder scan:** no TBD/TODO; every code step has full code; verification commands concrete. ✓
|
|
266
|
-
|
|
267
|
-
**3. Type consistency:** `canReplaceHost` new signature `{host, runnerObservation, childObservation, launchLeaseActive}` used consistently in Task 1 tests, Task 1 implementation, and matches Task 2's indirect usage (no direct call). `healServiceOverrides(probe, spawns)` helper name matches file. ✓
|