@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,270 +0,0 @@
|
|
|
1
|
-
# Root-cause report / Spec: recognize `ask_user_question` as a pending-question tool
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-20T22:10:00+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-20T22:24:00+08:00 — folded in R1-D1..D5
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:10:00+08:00 — emitter identified and installed locally; U1 deployment path corrected from npm release to the git-install clone
|
|
6
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:22:00+08:00 — R2-D1 reference precision (blocked-event emit site)
|
|
7
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:15:00+08:00 — user chose the hybrid guard (name OR questionnaire arg shape); design, acceptance matrix, risks and testability split updated
|
|
8
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:35:00+08:00 — R3-D1..D3 (deliverables list, superseded decision row, no-id fallback boundary)
|
|
9
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:52:00+08:00 — local-CR: shape bound wording matches the implementation (empty question string still matches)
|
|
10
|
-
|
|
11
|
-
- **Issue**: zhuxixi/pi-agent-board#221
|
|
12
|
-
- **Date**: 2026-10-20
|
|
13
|
-
- **Status**: draft — awaiting cross-review and user confirmation
|
|
14
|
-
- **Type**: bug fix (event-reducer allow-list + regression test); no data-model, schema, or UI change
|
|
15
|
-
|
|
16
|
-
## 1. Problem
|
|
17
|
-
|
|
18
|
-
A pi-style session blocked on a tool named `ask_user_question` stays in the **RUNNING**
|
|
19
|
-
group with the row summary `Ask_user_question`. It never reaches the **Needs answer**
|
|
20
|
-
group (`needs_input`, amber).
|
|
21
|
-
|
|
22
|
-
Reproduced at reducer level on `main` (9a197f4):
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
actual ask_user_question → semanticState=working, summary="Ask_user_question",
|
|
26
|
-
pendingQuestions=[], question=null,
|
|
27
|
-
currentTool={name:"ask_user_question"}, needsInput=false
|
|
28
|
-
control question → semanticState=needs_input, summary="Which approach?", needsInput=true
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Evidence trail: `issue-221/research/root-cause.md`, issue comment 6292835562.
|
|
32
|
-
|
|
33
|
-
## 2. Root cause
|
|
34
|
-
|
|
35
|
-
`src/core/events.mjs:21` defines an exact-membership allow-list:
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
const QUESTION_TOOL_NAMES = new Set(["ask_questions", "question", "questionnaire"]);
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Both gates are exact membership — `events.mjs:86` (`tool_execution_start`) and
|
|
42
|
-
`events.mjs:100` (`tool_execution_end`). `ask_user_question` misses both, so
|
|
43
|
-
`tool_execution_start` takes the generic branch (`currentTool` set, `semanticState =
|
|
44
|
-
"working"`), `preservePendingQuestion()` has nothing to promote, and `deriveSummary()`
|
|
45
|
-
falls back to `capitalize(name)`.
|
|
46
|
-
|
|
47
|
-
`questionFromArgs()` (`events.mjs:232`) already handles the arg shape
|
|
48
|
-
(`args.questions[].question`) — no extraction change is needed.
|
|
49
|
-
|
|
50
|
-
**Emitter (confirmed 2026-10-21)**: `ask_user_question` is registered by the pi extension
|
|
51
|
-
`@juicesharp/rpiv-ask-user-question` — `ask-user-question.ts:84`
|
|
52
|
-
(`ASK_USER_QUESTION_TOOL_NAME = "ask_user_question"`), registered at `:455-459`. It is not a
|
|
53
|
-
pi 1.3.0 built-in (pi ships no question tool; `question`/`questionnaire` come from
|
|
54
|
-
`pi-question-tool`). The extension is now installed on this machine: v2.13.9,
|
|
55
|
-
`~/.pi/agent/settings.json:90`, `~/.pi/agent/npm/node_modules/@juicesharp/rpiv-ask-user-question`,
|
|
56
|
-
and the tool is live in the session after `/reload` (session tool list contains
|
|
57
|
-
`question`, `questionnaire`, `ask_user_question`). Its arg schema
|
|
58
|
-
(`{ questions: [{ question, header, options, multiSelect }] }`) is exactly what
|
|
59
|
-
`questionFromArgs` already reads.
|
|
60
|
-
|
|
61
|
-
The defect stands regardless of the emitter: any unrecognized question-tool name silently
|
|
62
|
-
degrades grouping.
|
|
63
|
-
|
|
64
|
-
## 3. Design
|
|
65
|
-
|
|
66
|
-
### 3.1 Detection: name allow-list OR questionnaire arg shape
|
|
67
|
-
|
|
68
|
-
```js
|
|
69
|
-
/** Tool names whose interactive execution blocks on a user answer. */
|
|
70
|
-
const QUESTION_TOOL_NAMES = new Set(["ask_questions", "question", "questionnaire", "ask_user_question"]);
|
|
71
|
-
|
|
72
|
-
/** Questionnaire args shared by pi/Claude-Code style question tools. */
|
|
73
|
-
function isQuestionArgShape(args) {
|
|
74
|
-
const questions = Array.isArray(args?.questions) ? args.questions : null;
|
|
75
|
-
return Boolean(questions && questions.length > 0 && typeof questions[0]?.question === "string");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isQuestionToolCall(name, args) {
|
|
79
|
-
return QUESTION_TOOL_NAMES.has(name) || isQuestionArgShape(args);
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Why both: the name set keeps the known tools working even with empty args
|
|
84
|
-
(`ask_questions` is exercised with `args: {}` at `test/events.test.mjs:137`), while the shape
|
|
85
|
-
check survives a renamed or newly packaged question tool as long as it keeps the
|
|
86
|
-
`questions[0].question` schema. Neither replaces the other.
|
|
87
|
-
|
|
88
|
-
**End gate becomes name-agnostic**: `tool_execution_end` carries no `args`, so a
|
|
89
|
-
shape-matched start cannot be recognized at end time. Replace
|
|
90
|
-
`opts.interactive && QUESTION_TOOL_NAMES.has(event.toolName ?? "")` with
|
|
91
|
-
`opts.interactive` and let `removePendingQuestion(status, event.toolCallId)` filter by call
|
|
92
|
-
id (it is already a no-op when the id is not pending). Tool-call ids are unique per call, so
|
|
93
|
-
this cannot clear someone else's question.
|
|
94
|
-
|
|
95
|
-
The guarantee is exact for real worker events, which always carry `toolCallId`
|
|
96
|
-
(`pi` dist `agent-session` emits `{toolCallId,toolName,result,isError}`). The pre-existing
|
|
97
|
-
no-id fallback (`events.mjs:242/250` map a missing id to the literal `"ask_questions"`) is
|
|
98
|
-
unchanged; a malformed event without an id could therefore clear a pending entry that also
|
|
99
|
-
lacked one. That path is unreachable from the real event stream and is left as-is.
|
|
100
|
-
|
|
101
|
-
No other module reads question-tool names (`rg ask_questions src/` → only `events.mjs`).
|
|
102
|
-
|
|
103
|
-
### 3.2 Regression guard
|
|
104
|
-
|
|
105
|
-
Two new reducer tests in `test/events.test.mjs`, next to the existing
|
|
106
|
-
`interactive pi question tool …` test:
|
|
107
|
-
|
|
108
|
-
**Test A — name path (`ask_user_question`), pinned to the literal name**
|
|
109
|
-
|
|
110
|
-
- interactive start → `needs_input`, summary/question = first question text,
|
|
111
|
-
`pendingQuestions` length 1, `currentTool === null`, `projectViewState().needsInput === true`;
|
|
112
|
-
- interactive end for the same `toolCallId` → `working`, `question === null`,
|
|
113
|
-
`pendingQuestions === []`;
|
|
114
|
-
- detached reduction (no `interactive`) → `working`, `currentTool.name === "ask_user_question"`,
|
|
115
|
-
`pendingQuestions === []`.
|
|
116
|
-
|
|
117
|
-
**Test B — shape path (unknown name), plus the negative guard**
|
|
118
|
-
|
|
119
|
-
- interactive start with `toolName: "mystery_questionnaire"` and
|
|
120
|
-
`args: { questions: [{ question: "Ship it?", header: "Release", options: [{ label: "Yes" }] }] }`
|
|
121
|
-
→ `needs_input`, question `"Ship it?"`, one pending entry;
|
|
122
|
-
- interactive end with the **same unknown name** → `working`, cleared (proves the end gate is
|
|
123
|
-
call-id based, not name based);
|
|
124
|
-
- negative cases that must stay `working` with an empty `pendingQuestions`:
|
|
125
|
-
`{ questions: [] }`, `{ questions: "nope" }`, and a non-question tool (`bash`).
|
|
126
|
-
|
|
127
|
-
The literal name in Test A plus the unknown-name case in Test B are the guards: dropping the
|
|
128
|
-
name or the shape check fails `npm test` instead of silently regressing the dashboard.
|
|
129
|
-
|
|
130
|
-
### 3.3 Alternatives considered
|
|
131
|
-
|
|
132
|
-
| Alternative | Verdict | Reason |
|
|
133
|
-
|---|---|---|
|
|
134
|
-
| Arg-shape detection instead of a name set | Rejected | `ask_questions` is exercised with `args: {}` (`test/events.test.mjs:135`) and must still be `needs_input`; shape alone cannot cover it. |
|
|
135
|
-
| Hybrid (`set OR questions[] shape`) at drafting | Rejected, then superseded | Drafting-time verdict was to keep the diff minimal; the user gate on 2026-10-21 chose the hybrid, so this row is history only — the authoritative decision is the Adopted row below. |
|
|
136
|
-
| Configurable allow-list | Rejected | No config surface exists for reducer behavior; over-engineering for a one-name gap. |
|
|
137
|
-
| Export the set for tests | Rejected | The reducer is the public surface; tests already drive it directly. Exporting a private constant adds API for no gain. |
|
|
138
|
-
| Upstream `rpiv:ask-user:blocked` event instead of name detection | Rejected | Constant defined at `events.ts:38`, emitted at `ask-user-question.ts:64` (`pi.events.emit`) — an in-process bus event on the extension host. The board consumes JSON-mode worker stdout, which never carries pi bus events, so the channel is unreachable. |
|
|
139
|
-
| Name set OR arg-shape fallback | **Adopted** (user decision, 2026-10-21) | Survives a rename or a newly packaged question tool as long as the schema stays `questions[0].question`. Cost: any interactive tool with that exact shape is treated as a pending question; bounded by requiring a non-empty `questions[0].question` string and by the `opts.interactive` gate. |
|
|
140
|
-
|
|
141
|
-
### 3.4 Data flow (unchanged, verified)
|
|
142
|
-
|
|
143
|
-
| Step | Location | Result |
|
|
144
|
-
|---|---|---|
|
|
145
|
-
| interactive start, name in set OR questionnaire shape | `events.mjs:86` → `upsertPendingQuestion` → `preservePendingQuestion` | `needs_input`, `currentTool=null`, `question` = first question |
|
|
146
|
-
| summary | `derive.mjs:103` | question text wins over tool name |
|
|
147
|
-
| projection | `events.mjs:209` | `needsInput: true` |
|
|
148
|
-
| interactive end | `events.mjs:100` → `removePendingQuestion` (by call id) | back to `working`, `question=null` |
|
|
149
|
-
| inline reply while pending | `src/ui/dashboard.ts:2054 isAgentBusy` + `README.md:167/299` | reply rejected; answer via attach (pre-existing contract, now also applies to this name) |
|
|
150
|
-
| detached start/end | `opts.interactive` gate | unchanged `currentTool` behavior |
|
|
151
|
-
|
|
152
|
-
## 4. Acceptance matrix
|
|
153
|
-
|
|
154
|
-
| ID | Feature point | Verification | Concrete check | Pass criteria |
|
|
155
|
-
|----|---------------|--------------|----------------|---------------|
|
|
156
|
-
| A1 | `ask_user_question` start is treated as a pending question | automated (unit) | `npm test` — new test in `test/events.test.mjs` | `semanticState==="needs_input"`, `summary`/`question` equal `"Which approach?"`, `pendingQuestions.length===1`, `currentTool===null`, `projectViewState(s).needsInput===true` |
|
|
157
|
-
| A2 | `ask_user_question` end clears the pending question | automated (unit) | same test | `semanticState==="working"`, `question===null`, `pendingQuestions` empty |
|
|
158
|
-
| A3 | Detached reduction keeps legacy behavior | automated (unit) | same test (no `interactive` opt) | `semanticState==="working"`, `currentTool.name==="ask_user_question"`, `pendingQuestions` empty |
|
|
159
|
-
| A4 | No regression for the existing three names | automated (unit) | `npm test` (existing tests at `test/events.test.mjs:109/135/163/206/220`) | all pass unchanged |
|
|
160
|
-
| A5 | Shape fallback: unknown tool name with questionnaire args becomes a pending question | automated (unit) | Test B in `test/events.test.mjs` | `semanticState==="needs_input"`, question `"Ship it?"`, `pendingQuestions.length===1`, `currentTool===null` |
|
|
161
|
-
| A6 | End gate clears shape-matched calls without a name match | automated (unit) | Test B — `tool_execution_end` with the same unknown `toolName`/`toolCallId` | `semanticState==="working"`, `question===null`, `pendingQuestions` empty |
|
|
162
|
-
| A7 | Misleading args do not trigger the shape fallback | automated (unit) | Test B negative cases: `{questions: []}`, `{questions: "nope"}`, and `bash` | `semanticState==="working"`, `pendingQuestions` empty in all three |
|
|
163
|
-
| A8 | Repo-level static gate stays green | automated (static) | `npm run typecheck` | exit 0. Note: `tsconfig.json:9` sets `checkJs:false`, so this gate covers the `.ts` surface and is a repo regression check, not proof of this `.mjs` change; A1–A7 are the real proof |
|
|
164
|
-
| U1 | A real board session blocked on `ask_user_question` renders under Needs answer | user test | see §5 | row under **Needs answer** with the question text as summary; end of call returns it to RUNNING |
|
|
165
|
-
|
|
166
|
-
Full gate: `npm run verify` (typecheck + perf gate + tests + coverage thresholds + pack dry-run).
|
|
167
|
-
|
|
168
|
-
Failure branches: A2/A6 assert clear-on-end, A3 asserts detached stays generic, A7 asserts the
|
|
169
|
-
false-positive guard. No error path is added by this change, so no error-path item is owed.
|
|
170
|
-
|
|
171
|
-
## 5. User test (U1) — steps and timing
|
|
172
|
-
|
|
173
|
-
Now executable on this machine: the emitter is installed (`@juicesharp/rpiv-ask-user-question`
|
|
174
|
-
2.13.9) and the tool is live after `/reload`. The remaining precondition is the **board code**
|
|
175
|
-
itself — the board process loads from the git-install clone, not from this working checkout and
|
|
176
|
-
not from npm:
|
|
177
|
-
|
|
178
|
-
- `pi list` → `git:github.com/zhuxixi/pi-agent-board` at
|
|
179
|
-
`~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` (`~/.pi/agent/settings.json:85`), HEAD
|
|
180
|
-
`9a197f4` (= `main` at drafting time). That clone currently has a dirty `package-lock.json`
|
|
181
|
-
(4 deleted `"peer": true` lines), which must be discarded before pulling.
|
|
182
|
-
|
|
183
|
-
Timing: after the fix merges to `main`.
|
|
184
|
-
|
|
185
|
-
1. Update the clone the board actually loads, then restart the board/Pi:
|
|
186
|
-
`cd ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board && git checkout -- package-lock.json && git pull`
|
|
187
|
-
(or re-run `pi install git:github.com/zhuxixi/pi-agent-board`); confirm
|
|
188
|
-
`git log -1 --oneline` is the merged commit.
|
|
189
|
-
2. Restart Pi / the board service (an already-running pi process keeps the old extension code).
|
|
190
|
-
3. Dispatch a board session whose prompt forces a decision, so the model calls
|
|
191
|
-
`ask_user_question` with `questions[0].question` containing a short decision question
|
|
192
|
-
(e.g. "Which approach?"). The extension is user-level, so every session sees the tool.
|
|
193
|
-
4. Open the dashboard, then answer via attach.
|
|
194
|
-
|
|
195
|
-
Expected observation: the row sits under **Needs answer** (amber) with the question text as
|
|
196
|
-
its summary, `pendingQuestions` holds one entry, inline reply is refused (pre-existing
|
|
197
|
-
contract); after answering, the row returns to **RUNNING**.
|
|
198
|
-
|
|
199
|
-
If the merge has not happened by completion time, U1 is reported as `pending` (not passed);
|
|
200
|
-
A1–A5 still prove the reducer contract.
|
|
201
|
-
|
|
202
|
-
## 6. Non-goals
|
|
203
|
-
|
|
204
|
-
- No change to `detectNeedsInput()` text heuristics or `message_end` classification.
|
|
205
|
-
- The shape fallback does **not** apply to detached (non-interactive) runs: headless sessions
|
|
206
|
-
cannot answer, so they keep `currentTool` display.
|
|
207
|
-
- No rename of the `"ask_questions"` fallback ids in `upsertPendingQuestion` /
|
|
208
|
-
`removePendingQuestion` (`events.mjs:242/250`).
|
|
209
|
-
- No config surface or UI affordance for arbitrary question-tool names.
|
|
210
|
-
- No change to attach/reply/answer transport; only reducer classification changes.
|
|
211
|
-
|
|
212
|
-
## 7. Risks and boundaries
|
|
213
|
-
|
|
214
|
-
- **False positive (accepted)**: any interactive tool whose args are
|
|
215
|
-
`{ questions: [{ question: <string> }] }` is treated as a pending question, even if it is
|
|
216
|
-
not asking the user. Bounded by the `opts.interactive` gate and by requiring the first
|
|
217
|
-
`question` to be a string; an empty or whitespace-only string still matches and falls back
|
|
218
|
-
to the generic "Answer the pending question" text, exactly like the name-set path. No
|
|
219
|
-
installed extension is known to declare that shape for a non-question purpose today
|
|
220
|
-
(research §4), but absence over third-party code is not provable, so the guard is the bound
|
|
221
|
-
itself, not a completeness claim.
|
|
222
|
-
- **End gate is now name-agnostic**: every interactive `tool_execution_end` calls
|
|
223
|
-
`removePendingQuestion(id)`. For real worker events (which always carry `toolCallId`) ids are
|
|
224
|
-
unique per call, so this clears the matching entry or does nothing; it cannot clear another
|
|
225
|
-
call's question (covered by A6 and by the existing multi-question test at
|
|
226
|
-
`test/events.test.mjs:135`). The legacy no-id fallback is unchanged and is documented in §3.1
|
|
227
|
-
as the only way a cross-clear could occur.
|
|
228
|
-
- **Shape fallback is a heuristic**: a tool that renames its `questions` payload field
|
|
229
|
-
(e.g. to `prompts`) still regresses. Accepted; a schema change is a protocol change, not a
|
|
230
|
-
rename, and cannot be absorbed by a reducer heuristic.
|
|
231
|
-
- **Detached runs** are unaffected — headless sessions cannot answer, so they must keep
|
|
232
|
-
`currentTool` display.
|
|
233
|
-
- **Only JSON-mode worker events reach the reducer.** The extension's own
|
|
234
|
-
`rpiv:ask-user:blocked` bus event is in-process and never appears in worker stdout, so the
|
|
235
|
-
board cannot rely on it.
|
|
236
|
-
- **Inline reply is rejected while the question is pending** (`README.md:167/299`,
|
|
237
|
-
`src/ui/dashboard.ts:2054 isAgentBusy`). This is pre-existing behavior for pending
|
|
238
|
-
questions and now applies to `ask_user_question` rows too; answer via attach. Expected,
|
|
239
|
-
not a regression.
|
|
240
|
-
|
|
241
|
-
## 8. Testability split
|
|
242
|
-
|
|
243
|
-
The reducer is already a pure-function seam: `createRunStatus(config, pid, now)` builds
|
|
244
|
-
state, `reduceEvent(status, event, now, opts)` mutates it, `projectViewState(status, now)`
|
|
245
|
-
projects it. Two new module-private pure helpers carry the new logic —
|
|
246
|
-
`isQuestionArgShape(args)` and `isQuestionToolCall(name, args)` — and are driven through
|
|
247
|
-
`reduceEvent`, the public surface, exactly like the existing `questionFromArgs` (which is
|
|
248
|
-
also private and tested through the reducer).
|
|
249
|
-
|
|
250
|
-
Planned split:
|
|
251
|
-
|
|
252
|
-
| Unit | Purity | Test seam |
|
|
253
|
-
|---|---|---|
|
|
254
|
-
| `isQuestionArgShape(args)` | pure, no I/O | via `reduceEvent` start events (Test B positives/negatives) |
|
|
255
|
-
| `isQuestionToolCall(name, args)` | pure, no I/O | via `reduceEvent`; name path in Test A, shape path in Test B |
|
|
256
|
-
| start handler branch | side-effect-free over `RunStatus` | `reduceEvent` + `projectViewState` assertions |
|
|
257
|
-
| end handler (call-id removal) | side-effect-free over `RunStatus` | `reduceEvent` end events (Test A, Test B, existing multi-question test) |
|
|
258
|
-
|
|
259
|
-
Test boundary: `test/events.test.mjs` only. No export is added to `src/core/events.mjs`;
|
|
260
|
-
the helpers stay private and are covered through the reducer, matching the existing pattern.
|
|
261
|
-
|
|
262
|
-
## 9. Deliverables
|
|
263
|
-
|
|
264
|
-
- `src/core/events.mjs` — add `ask_user_question` to `QUESTION_TOOL_NAMES`; add the private
|
|
265
|
-
`isQuestionArgShape(args)` / `isQuestionToolCall(name, args)` helpers; gate
|
|
266
|
-
`tool_execution_start` on `isQuestionToolCall`; make `tool_execution_end` clear by
|
|
267
|
-
`toolCallId` under `opts.interactive` only.
|
|
268
|
-
- `test/events.test.mjs` — Test A (name path) and Test B (shape path + negative guards).
|
|
269
|
-
- `docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md` — this design,
|
|
270
|
-
committed with the fix (repo precedent: #213/#214/#215 each ship their spec with the PR).
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
# Root-cause report / Spec: monotonic foreground state rebuild (stale "Needs answer")
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-22T23:50:00+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T00:05:00+08:00 — folded in R1-G1..G3 and R1-D1..D4
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T00:20:00+08:00 — R2-D1..D3 (A4 clock pin, timestamp-bound wording, forget wiring acceptance)
|
|
6
|
-
|
|
7
|
-
- **Issue**: zhuxixi/pi-agent-board#225
|
|
8
|
-
- **Date**: 2026-10-22
|
|
9
|
-
- **Status**: draft — awaiting cross-review and user confirmation
|
|
10
|
-
- **Type**: bug fix (foreground state mirroring); no reducer, schema, or UI change
|
|
11
|
-
|
|
12
|
-
## 1. Problem
|
|
13
|
-
|
|
14
|
-
A foreground row that transiently becomes `needs_input` can stay in **Needs answer** for
|
|
15
|
-
the rest of the turn after the question is answered. Live evidence (issue #225):
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
22:56:21 needs_input pending=[call_.../1]
|
|
19
|
-
22:56:44 working pending=[] ← tool_execution_end cleared it
|
|
20
|
-
22:56:44 needs_input pending=[call_.../1] ← stale projection re-applied
|
|
21
|
-
22:57:04 idle pending=[] ← agent_end finally rewrote it
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
The reducer is correct (#221); the resurrection happens in the foreground mirror.
|
|
25
|
-
|
|
26
|
-
## 2. Root cause
|
|
27
|
-
|
|
28
|
-
1. `syncRowEvent` (`src/runtime/service.mjs:1533`) rebuilds status from disk on every
|
|
29
|
-
event: `statusFromRow(row)` (`:1535`).
|
|
30
|
-
2. `statusFromRow` (`:814-843`) copies `semanticState` / `summary` / `question` /
|
|
31
|
-
`pendingQuestions` but **not `updatedAt`**, so the rebuild has no freshness signal.
|
|
32
|
-
3. `writeForegroundState` (`:854-880`) stamps a new `updatedAt` through
|
|
33
|
-
`projectViewState(status, Date.now(), …)`, and every caller fires it as
|
|
34
|
-
`void writeForegroundState(...)` (`:1571`, `:1629`). Two events can therefore have two
|
|
35
|
-
`sync_foreground` commands in flight, and the older projection can materialize last.
|
|
36
|
-
4. The next event reduces onto that stale status (it cannot tell it is stale) and writes it
|
|
37
|
-
back; the pending entry survives until `agent_end` finalizes the row.
|
|
38
|
-
5. `foregroundPreviewCache` (#113) restores only `latestAssistantPreview` and
|
|
39
|
-
`lastAgentActivityAt`, keyed on `lastAgentActivityAt` — tool events never stamp that
|
|
40
|
-
field, so question state is uncovered.
|
|
41
|
-
|
|
42
|
-
Additional constraint: `mark_completed` / `mark_holding` / `patch_fields`
|
|
43
|
-
(`src/core/state-commands.mjs`) do **not** stamp `state.updatedAt`, so a timestamp-only
|
|
44
|
-
guard could resurrect a pre-verdict projection over a manual verdict. The manual-verdict
|
|
45
|
-
fence (`isManualVerdict`) must gate the adoption.
|
|
46
|
-
|
|
47
|
-
## 3. Design
|
|
48
|
-
|
|
49
|
-
### 3.1 New pure module: `src/core/foreground-projection-cache.mjs`
|
|
50
|
-
|
|
51
|
-
Module-level shared instance, same rationale as `foreground-preview-cache` (service
|
|
52
|
-
instances are created per call, so a per-instance cache is discarded between events).
|
|
53
|
-
|
|
54
|
-
The cache stores a **status-shaped snapshot**, not the `ViewState` projection: the
|
|
55
|
-
projection calls the active tool `latestTool`, while `RunStatus` calls it `currentTool` with
|
|
56
|
-
a `summary` (`src/core/events.mjs:225`, `src/runtime/service.mjs:826`). Storing the status
|
|
57
|
-
keeps the whitelist aligned with `RunStatus` and avoids an implicit shape mapping.
|
|
58
|
-
|
|
59
|
-
```js
|
|
60
|
-
createForegroundProjectionCache(): {
|
|
61
|
-
nextStamp(viewId): number // strictly increasing per view
|
|
62
|
-
remember(viewId, status, stamp): void // snapshot the whitelisted fields
|
|
63
|
-
adopt(viewId, status): boolean // copy the snapshot when strictly newer
|
|
64
|
-
forget(viewId): boolean
|
|
65
|
-
clear(): void
|
|
66
|
-
size(): number
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
- `nextStamp(viewId)` returns `max(Date.now(), lastStamp(viewId) + 1)`. Using it for
|
|
71
|
-
`projectViewState`'s `now` argument makes every foreground `updatedAt` strictly increasing
|
|
72
|
-
per view, so two events in the same millisecond can never tie and an older projection can
|
|
73
|
-
never outrank a newer one inside this process.
|
|
74
|
-
- `remember` snapshots this whitelist from the status:
|
|
75
|
-
`semanticState`, `processState`, `summary`, `currentTool`, `question`,
|
|
76
|
-
`pendingQuestions`, `error`, `lastActivityAt`, `latestAssistantPreview`,
|
|
77
|
-
`lastAgentActivityAt`, plus the `stamp`. Nested values (`currentTool`,
|
|
78
|
-
`pendingQuestions`) are cloned **on remember**, so a later mutation of the caller's status
|
|
79
|
-
cannot leak into the cache.
|
|
80
|
-
- `adopt(viewId, status)` compares `entry.stamp` with `status.updatedAt ?? 0`. Only a
|
|
81
|
-
strictly newer entry is adopted; it copies the same whitelist (cloning nested values
|
|
82
|
-
again, so `adopt` → caller mutation → second `adopt` is stable) and sets
|
|
83
|
-
`status.updatedAt = entry.stamp`. Returns whether anything changed.
|
|
84
|
-
- Nothing else (evidence `review`, `diagnostics`, `autoState`, `lastVisitedAt`, `steering`,
|
|
85
|
-
`followUps`) is touched — those have their own writers and freshness rules.
|
|
86
|
-
|
|
87
|
-
### 3.2 Wiring in `src/runtime/service.mjs`
|
|
88
|
-
|
|
89
|
-
| Point | Change |
|
|
90
|
-
|---|---|
|
|
91
|
-
| `statusFromRow` (`:814`) | add `updatedAt: typeof s.updatedAt === "number" ? s.updatedAt : 0` (in-memory freshness only; `RunStatus` gains an optional `updatedAt` typedef in `src/core/types.mjs`) |
|
|
92
|
-
| `writeForegroundState` (`:854`) | `const stamp = foregroundProjectionCache.nextStamp(row.meta.id)`; pass `stamp` instead of `Date.now()` to `projectViewState`; then `foregroundProjectionCache.remember(row.meta.id, status, stamp)` |
|
|
93
|
-
| `syncRowEvent` (`:1540`) | after the preview backfill: `foregroundProjectionCache.adopt(row.meta.id, status)` before `reduceEvent` |
|
|
94
|
-
| cache eviction (`:775`, `:2131`) | call `foregroundProjectionCache.forget(viewId)` next to the existing `foregroundPreviewCache.forget(viewId)` |
|
|
95
|
-
|
|
96
|
-
`row` is loaded from disk at the event entry (`rowForSession` → `loadRow`), before any
|
|
97
|
-
`await`, so `row.state` is as fresh as `readState(root, viewId)` at that point.
|
|
98
|
-
|
|
99
|
-
The preview cache stays as-is: its #113 semantics (gap-fill when the timestamp ties) are
|
|
100
|
-
still useful, and folding it into the new cache is a separate refactor (non-goal).
|
|
101
|
-
|
|
102
|
-
### 3.3 Why not serialize the writes (issue option 2)
|
|
103
|
-
|
|
104
|
-
A module-level write queue would fix the ordering at the source, but it adds a new failure
|
|
105
|
-
mode (one stalled coordinator round trip blocks every later event) and needs its own
|
|
106
|
-
backoff/timeout policy. The monotonic rebuild fixes the *read* side, so it also covers
|
|
107
|
-
future writers (dashboard patches, reconcile) and any out-of-order arrival that is not a
|
|
108
|
-
`sync_foreground` command. Option 2 remains a possible follow-up, not this fix.
|
|
109
|
-
|
|
110
|
-
### 3.4 Manual verdicts (boundary, no new fence)
|
|
111
|
-
|
|
112
|
-
Adoption is a pure comparison: adopt only when `entry.stamp > status.updatedAt`. Because
|
|
113
|
-
`nextStamp` can run ahead of the wall clock during a sub-millisecond burst, a writer that
|
|
114
|
-
stamps with a bare `Date.now()` *can* compare older than the cache — the timestamp does not
|
|
115
|
-
order against every writer.
|
|
116
|
-
|
|
117
|
-
Manual verdicts do not rely on that comparison. Under the coordinator, `mark_completed` /
|
|
118
|
-
`mark_holding` writes are materialized without stamping `updatedAt`
|
|
119
|
-
(`state-commands.mjs:288-334`), and any later `sync_foreground` from `service` is rejected
|
|
120
|
-
with `manual_fence` (`state-commands.mjs:282-285`). In coordinator-disabled mode the direct
|
|
121
|
-
writers (`completeViewDirect`, `holdViewDirect`, `archiveStateDirect`, reconcile) *do* stamp
|
|
122
|
-
`Date.now()`, but that path already lets a foreground event overwrite the semantic state
|
|
123
|
-
through `reduceEvent` today, independent of this cache, so adoption adds no new way to
|
|
124
|
-
resurrect a verdict. No extra fence is added; the existing `manual_fence` and the
|
|
125
|
-
`agent_end` branch fence remain the protections, and A5 pins the observable behavior.
|
|
126
|
-
|
|
127
|
-
### 3.5 Data flow after the change
|
|
128
|
-
|
|
129
|
-
| Event | Behavior |
|
|
130
|
-
|---|---|
|
|
131
|
-
| start question | `reduceEvent` → `needs_input`; write uses stamp S1 and remembers it |
|
|
132
|
-
| end question | rebuild from disk (maybe stale) → adopt if cache is newer → `reduceEvent` clears pending → write uses S2 > S1 |
|
|
133
|
-
| subsequent tool event | rebuild carries S2 → adopt is a no-op (equal) → `reduceEvent` keeps `working`; never resurrects `needs_input` |
|
|
134
|
-
| dashboard write | disk `updatedAt` is newer than the cache → nothing adopted |
|
|
135
|
-
| agent_end | finalize as today |
|
|
136
|
-
|
|
137
|
-
## 4. Acceptance matrix
|
|
138
|
-
|
|
139
|
-
| ID | Feature point | Verification | Concrete check | Pass criteria |
|
|
140
|
-
|----|---------------|--------------|----------------|---------------|
|
|
141
|
-
| A1 | `remember`/`adopt` respect strict freshness | automated (unit) | `test/foreground-projection-cache.test.mjs` | newer stamp adopted (`true`); equal and older stamps not adopted (`false`), status unchanged |
|
|
142
|
-
| A2 | Whitelist copy is exact and isolated | automated (unit) | same file | all whitelisted fields copied; non-whitelisted untouched; mutating `status.pendingQuestions[0]` after `adopt` does not change a second `adopt`; mutating the status after `remember` does not change the stored snapshot |
|
|
143
|
-
| A3 | `nextStamp` is strictly increasing per view | automated (unit) | same file | two calls in a row return `b > a`, even when `Date.now()` is stubbed to a constant; different view ids do not share the counter |
|
|
144
|
-
| A4 | Out-of-order materialization no longer resurrects `needs_input` | automated (integration) | `test/service.test.mjs`, coordinator disabled, `Date.now` pinned to a constant for the whole test so the pre-fix same-millisecond tie is deterministic: question start → capture `state.json` (its `updatedAt` is the start stamp) → question end → `writeState(captured snapshot)` (an older stamp) → `tool_execution_start` for `bash` | persisted state is `working`, `pendingQuestions` empty, `needsInput` false. Pre-fix this asserts `needs_input` (the disk rebuild wins the tie), post-fix the monotonic stamp adopts the newer cached status |
|
|
145
|
-
| A5 | A manual verdict survives a foreground event | automated (integration) | `test/service.test.mjs`: seed the projection cache with an accepted-but-unmaterialized beat, write `completed` + `autoState: null` with an older `updatedAt`, then a `tool_execution_start` that adopts the cached snapshot | persisted state stays `completed` (coordinator answers `manual_fence`). Pins the fence, not adoption isolation — adoption has no independent observable there |
|
|
146
|
-
| A6 | `forget` drops the entry and is wired into archive | automated (unit + integration) | cache test: `remember` → `forget` → `adopt` false; `test/service.test.mjs`: `archiveByState` on a remembered view | `adopt` returns `false` after `forget`; after archiving, `foregroundProjectionCache.size()` no longer counts that view |
|
|
147
|
-
| A7 | Existing foreground behavior unchanged | automated (unit/integration) | `npm test` (existing `syncForegroundEvent` and preview-cache tests) | all pass |
|
|
148
|
-
| A8 | Repo static gate | automated (static) | `npm run typecheck` | exit 0 (`checkJs:false`, so this is a repo regression gate, not proof of the `.mjs` change) |
|
|
149
|
-
| U1 | Live: answering a question returns the row to RUNNING before the turn ends | user test | see §5 | row leaves **Needs answer** within ~1s of the answer and stays RUNNING until the turn ends |
|
|
150
|
-
|
|
151
|
-
Full gate: `npm run verify` (typecheck + perf + tests + coverage thresholds + pack dry-run).
|
|
152
|
-
|
|
153
|
-
Failure branches: A1/A2 assert the negative side (equal/older stamps are not adopted, nested
|
|
154
|
-
values cannot leak), A5 asserts the manual-verdict boundary, A6 asserts cache eviction. No
|
|
155
|
-
new error path is introduced.
|
|
156
|
-
|
|
157
|
-
## 5. User test (U1)
|
|
158
|
-
|
|
159
|
-
Executable after merge, on this machine (the board clone at
|
|
160
|
-
`~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` is updated to the merged commit and Pi
|
|
161
|
-
restarted):
|
|
162
|
-
|
|
163
|
-
1. In a session, trigger a question (`ask_user_question` or `question`).
|
|
164
|
-
2. Watch the row enter **Needs answer**; answer it in the dashboard/attach.
|
|
165
|
-
3. Keep watching for the rest of the turn.
|
|
166
|
-
|
|
167
|
-
Expected: the row returns to **RUNNING** within about a second of the answer — not at
|
|
168
|
-
`agent_end`. Repeat with a nested call (question asked from inside `codemode`) to exercise
|
|
169
|
-
the wider window.
|
|
170
|
-
|
|
171
|
-
If the environment cannot be updated at completion time, U1 is reported as `pending`.
|
|
172
|
-
|
|
173
|
-
## 6. Non-goals
|
|
174
|
-
|
|
175
|
-
- No change to `reduceEvent`, `deriveSummary`, or the question-tool detection (#221 is done).
|
|
176
|
-
- No serialization of `sync_foreground` writes (issue option 2) and no coordinator changes.
|
|
177
|
-
- No removal or rewrite of `foreground-preview-cache.mjs` (#113 behavior preserved).
|
|
178
|
-
- No changes to manual verdicts, auto-state classification, or evidence/diagnostics writes.
|
|
179
|
-
|
|
180
|
-
## 7. Risks and boundaries
|
|
181
|
-
|
|
182
|
-
- **Clock ties**: prevented inside this process by `nextStamp` (strictly increasing per
|
|
183
|
-
view). Ties are still possible with a *different* writer (another Pi process) whose
|
|
184
|
-
`Date.now()` equals ours; then neither side is "strictly newer" and the disk value wins,
|
|
185
|
-
which is the pre-existing behavior. Cross-process ordering is not guaranteed.
|
|
186
|
-
- **Cross-process writers**: another Pi process mirroring the same view writes its own
|
|
187
|
-
`updatedAt`; this cache only protects against out-of-order writes from *this* process.
|
|
188
|
-
Outside the supported single-host model; documented, not fixed.
|
|
189
|
-
- **In-process writers with a bare `Date.now()`**: the auto-state classifier
|
|
190
|
-
(`applyAutoStateToViewState`, `src/core/auto-state.mjs:254`) stamps a wall-clock
|
|
191
|
-
`updatedAt`, which can compare older than a cache stamp that ran ahead within the same
|
|
192
|
-
millisecond. A late foreground tool event would then adopt the pre-classification
|
|
193
|
-
snapshot — but such an event already clobbers the classification through `reduceEvent`
|
|
194
|
-
today (it sets `working` and `alive`), so this is not a new resurrection path.
|
|
195
|
-
- **Manual verdicts**: protected by the coordinator's `manual_fence` and the `agent_end`
|
|
196
|
-
branch fence, not by timestamps. Coordinator materialization does not stamp `updatedAt`
|
|
197
|
-
for verdict writes; coordinator-disabled direct writers do stamp `Date.now()`, which can
|
|
198
|
-
compare older than a cache stamp that ran ahead of the clock — that path is not ordered by
|
|
199
|
-
this cache (see §3.4, A5).
|
|
200
|
-
- **Memory**: one small object per view, evicted with the existing `forget` sites.
|
|
201
|
-
|
|
202
|
-
## 8. Testability split
|
|
203
|
-
|
|
204
|
-
| Unit | Purity | Test seam |
|
|
205
|
-
|---|---|---|
|
|
206
|
-
| `createForegroundProjectionCache()` (`nextStamp` / `remember` / `adopt` / `forget`) | pure (Map in/out, tested with a stubbed clock) | direct unit tests (`test/foreground-projection-cache.test.mjs`) |
|
|
207
|
-
| `statusFromRow` `updatedAt` passthrough | pure | exercised through `syncForegroundEvent` (A4) |
|
|
208
|
-
| adoption in `syncRowEvent` | side-effect-free call + thin I/O shell | `syncForegroundEvent` with coordinator disabled (A4), deterministic `writeState` between events; A5 uses the tracked coordinator |
|
|
209
|
-
| stamp assignment in `writeForegroundState` | pure call inside the existing shell | A4 (the repro fails without monotonic stamps: the stale snapshot would tie) |
|
|
210
|
-
| write-side `remember` | pure call inside the existing shell | A4 (the repro fails without it) |
|
|
211
|
-
|
|
212
|
-
Test boundary: new pure module tests + additions to `test/service.test.mjs`; no new exports
|
|
213
|
-
from the service.
|
|
214
|
-
|
|
215
|
-
## 9. Deliverables
|
|
216
|
-
|
|
217
|
-
- `src/core/foreground-projection-cache.mjs` — new pure module.
|
|
218
|
-
- `src/runtime/service.mjs` — `statusFromRow` `updatedAt`; `nextStamp` + `remember` in
|
|
219
|
-
`writeForegroundState`; `adopt` in `syncRowEvent`; `forget` at the eviction sites.
|
|
220
|
-
- `src/core/types.mjs` — optional `updatedAt` on the `RunStatus` typedef (in-memory
|
|
221
|
-
freshness field).
|
|
222
|
-
- `test/foreground-projection-cache.test.mjs` — new unit tests (A1–A3, A6).
|
|
223
|
-
- `test/service.test.mjs` — A4/A5 integration tests.
|
|
224
|
-
- `docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md` — this
|
|
225
|
-
design, committed with the fix.
|