instar 1.3.804 → 1.3.806

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dashboard/index.html +19 -1
  2. package/dashboard/subscriptions.js +513 -67
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +43 -0
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/CoherenceJournal.d.ts +14 -0
  7. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  8. package/dist/core/CoherenceJournal.js +33 -0
  9. package/dist/core/CoherenceJournal.js.map +1 -1
  10. package/dist/core/EnrollmentWizard.d.ts +23 -0
  11. package/dist/core/EnrollmentWizard.d.ts.map +1 -1
  12. package/dist/core/EnrollmentWizard.js +42 -1
  13. package/dist/core/EnrollmentWizard.js.map +1 -1
  14. package/dist/core/JournalSyncApplier.d.ts +14 -0
  15. package/dist/core/JournalSyncApplier.d.ts.map +1 -1
  16. package/dist/core/JournalSyncApplier.js +33 -0
  17. package/dist/core/JournalSyncApplier.js.map +1 -1
  18. package/dist/core/ReplicatedPeerStreamReader.d.ts +29 -1
  19. package/dist/core/ReplicatedPeerStreamReader.d.ts.map +1 -1
  20. package/dist/core/ReplicatedPeerStreamReader.js +175 -0
  21. package/dist/core/ReplicatedPeerStreamReader.js.map +1 -1
  22. package/dist/server/routes.d.ts.map +1 -1
  23. package/dist/server/routes.js +128 -30
  24. package/dist/server/routes.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +46 -46
  27. package/upgrades/1.3.805.md +41 -0
  28. package/upgrades/1.3.806.md +30 -0
  29. package/upgrades/eli16/replicated-witness-index.md +19 -0
  30. package/upgrades/side-effects/replicated-witness-index.md +83 -0
  31. package/upgrades/side-effects/subs-setup-flow-ux.md +79 -0
  32. package/upgrades/subs-setup-flow-ux.eli16.md +31 -0
@@ -0,0 +1,79 @@
1
+ # Side-Effects Review — Subscriptions "Set up" flow: never-clobber refresh, in-cell complete flow, wrong-account guard surfacing, unmistakable terminal states, record⟂pane liveness (topic 29836 D1–D5)
2
+
3
+ **Version / slug:** `subs-setup-flow-ux`
4
+ **Date:** `2026-07-10`
5
+ **Author:** Echo (instar-dev agent)
6
+ **Second-pass reviewer:** not required (no new blocking authority — see §4; the one identity gate touched is pre-existing, converged, and only has its verdict SURFACED)
7
+
8
+ ## Summary of the change
9
+
10
+ Fixes the five operator-observed (screenshot-proven, topic 29836, 2026-07-10) defects in the dashboard Subscriptions tab's account×machine "Set up" enrollment flow. **D1** — the 30s poll re-rendered the matrix/panel out from under an open interaction (PIN input reverted to a button mid-typing; the code-paste step swapped for "◷ Signing in…"); fixed as a RULE: new F9 primitives `hasOpenInteraction` + `updateCountdowns` in `dashboard/subscriptions.js`, and the controller rebuilds a surface only while no interaction is open (episode marker `data-interaction-open`, focused text-entry, or dirty field), merging countdowns in place otherwise. **D2** — the flow's continuation lived only in the bottom "Pending logins" panel; now the matrix cell carries the COMPLETE flow (sign-in link, expected-account notice, code input, TTL countdown, two-codes notice, Cancel), rendered from SERVER pending-login state via `appendCellSignInFlow` so it also survives reloads and rebuilds. **D3** — the wrong-account hazard: the UI now states which account the OAuth page MUST show (from the enrollment record's `expectedEmail`, passed through start-cell responses), and the pre-existing S7 email-gate's held verdict now carries `expected`/`got` through `EnrollmentWizard.completeFollowMe` → the submit-code response → plain-language copy naming BOTH accounts; oracle-unavailable keeps failing CLOSED with honest "couldn't confirm" copy. **D4** — invisible success: explicit terminal presentations (in-cell "✓ All set" + transient `just-verified` highlight bridging until the pool read catches up; explicit held/expired/broken states; durable done/failed cards in the pending panel). **D5** — the re-sign-in flow: pending logins are annotated with `paneAlive` (record⟂pane reconciliation; tri-state, fail-toward-unknown); a dead-pane record renders an explicit needs-restart state and code-submit against it answers a machine-readable 409 `pane-dead`; enroll/start enforces single-attempt discipline (reuse a live attempt / supersede a dead one atomically — codes can never cross between parallel PKCE attempts); a new `sweepFollowMeCompletions` handles the already-authorized short-circuit (credential lands with no code ever shown) through the SAME identity gate; validated completions UPSERT an existing pool account (re-auth previously crashed on `add()`'s duplicate-id refusal and stranded the flow); wording floors (account by email, machine by nickname — never a raw `m_<hex>` id; errors only reference affordances that exist on their surface). Files: `dashboard/subscriptions.js`, `dashboard/index.html`, `src/server/routes.ts`, `src/core/EnrollmentWizard.ts`, `src/commands/server.ts`, `docs/specs/dashboard-ux-standard.md` (F9), `docs/STANDARDS-REGISTRY.md`, tests in all three tiers.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `S7 email gate (validateEnrolledAccountEmail / completeFollowMe)` — pass-through — the pre-existing fail-closed identity gate is UNCHANGED in logic; its verdict now carries `expected`/`got` up to the surface. No new accept/reject behavior.
15
+ - `submit-code pane-readiness guard (routes.ts)` — modify (split, not weakened) — the existing fail-closed 409 is split into two machine-readable refusals: `pane-dead` (capture null/empty) vs `pane-not-ready` (live but not at the code prompt). Every input refused before is still refused; nothing new is accepted.
16
+ - `enroll/start single-attempt pre-check (routes.ts)` — add — a re-request while a HEALTHY attempt is live returns THAT attempt (idempotent read, not a block); only a provably-dead-pane attempt is superseded (abandon + fresh drive). Fails toward REUSE when liveness is unverifiable — it can never kill a healthy attempt on a capture hiccup.
17
+ - `start-cell reuse gate (routes.ts)` — modify — reuse additionally requires `paneAlive !== false` (same fail-toward-reuse posture).
18
+ - `sweepFollowMeCompletions (EnrollmentWizard + server.ts timer)` — add — detects a landed credential and drives the EXISTING completeFollowMe gate; it holds no accept authority of its own (a mismatch is held exactly as before).
19
+ - `F9 interaction hold (dashboard/subscriptions.js)` — add — client-side rendering-timing rule only; it gates WHEN the DOM rebuilds, never what the server accepts or what messages flow.
20
+
21
+ ---
22
+
23
+ ## 1. Over-block
24
+
25
+ - `pane-dead`/`pane-not-ready` split: no legitimate input newly rejected — the predicate set is identical to the old combined check; only the refusal is classified. A pane that IS at the code prompt still accepts the code.
26
+ - enroll/start reuse: a re-request that previously minted a duplicate (or 500'd on the store's duplicate-pending refusal after killing the old pane) now gets the live attempt back — strictly fewer refusals.
27
+ - Supersede: only fires when `paneAlive === false` (capture EXPLICITLY says the tmux session is gone). An unverifiable capture (`null`) reuses — no healthy attempt is ever abandoned on uncertainty.
28
+ - F9 hold: a surface with a permanently-dirty field would stop rebuilding — mitigated: terminal paths clear inputs, the PIN stage has an explicit Back, episodes always resolve (validated/held/broken/expired via the reconciler), and countdown merges keep held surfaces honest meanwhile. Worst case is a stale (not wrong) panel until the operator finishes or backs out.
29
+ - No issue identified beyond the above.
30
+
31
+ ## 2. Under-block
32
+
33
+ - `paneAlive` is advisory/observational (tri-state, null on any doubt): a pane that dies BETWEEN the poll and the code submit still reaches the submit-time fail-closed capture check — the authoritative guard is unchanged and still refuses.
34
+ - The completion sweep runs on the 5-min reissue cadence: a short-circuited login can sit "signing in" up to ~5 min before flipping — bounded delay, not a miss (the submit-code 30s poll covers the code path).
35
+ - A zombie whose pane LOOKS alive (a different process reusing the same tmux session name) would be reused; the submit-time positive prompt check (`paste`+`code`, non-shell last line) still refuses typing into it. No issue identified beyond these.
36
+
37
+ ## 3. Level-of-abstraction fit
38
+
39
+ - The identity decision stays where it already lives (the S7 gate in `EnrollmentWizard`/`AccountFollowMeEmailGate`) — the dashboard and routes only surface its verdict. No parallel identity check was added.
40
+ - Pane liveness lives server-side at the routes layer (the only place with tmux access), exposed as data; the client renders it. The client never infers liveness.
41
+ - The F9 rule lives in the shared dashboard module as exported primitives (not inlined per-handler) so other polling tabs can adopt it — the standard names this as the migration path.
42
+ - The completion sweep reuses the existing reissue timer and the existing completion chokepoint rather than adding a second lifecycle owner.
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ Compliant. No new brittle check holds blocking authority: the F9 hold gates rendering timing only; `paneAlive` is a SIGNAL consumed for presentation and for the supersede decision whose failure direction is reuse (non-destructive); the pane-readiness refusal predicate is pre-existing (and was already reviewed as the FD13 fail-closed guard); the S7 email gate — the one true authority here — is untouched in logic. The supersede path's only destructive act (abandoning a pending record) requires an EXPLICIT `false` liveness verdict, and the record it abandons is by construction unusable (no pane to receive its code). Ref: `docs/signal-vs-authority.md`.
47
+
48
+ ## 5. Interactions
49
+
50
+ - The reissue sweep can re-issue an expired login while a cell shows "expired": the next poll renders the fresh in-progress flow from server state — the transient expired presentation yields to live server truth (transient states sit BELOW in-progress in the model's precedence, except held/cant-resolve which are operator-terminal until retap).
51
+ - submit-code's in-flight mutex vs the completion sweep: `complete()` is single-winner (the store's terminal transition); the loser sees not-found and stands down. The upsert makes double-completion idempotent at the pool.
52
+ - The cancel relay (shipped feature) is preserved: the in-cell Cancel uses the same `data-matrix-cancel` handler/route; cancel-while-submitting still gets the existing 409 stand-aside.
53
+ - The matrix "Cancel" e2e (`matrix-cell-cancel-alive`) and the start-cell idempotency tests were re-run green; the reuse gate's new pane condition degrades to the old behavior when no sessionManager exists (tri-state null).
54
+ - Double-fire: `recordSubmitOutcome` is the single chokepoint for both submit surfaces (cell + panel), so an outcome lands once in transients/cards regardless of which surface drove it.
55
+
56
+ ## 6. External surfaces
57
+
58
+ - API responses gain ADDITIVE fields only (`expectedEmail`/`ttlExpiresAt`/`notice`/`kind` on start-cell; `expected`/`got` on held; `email` on validated; `paneAlive` + self `machineNickname` on pending-logins; `code` on the two 409s; `reused` on enroll/start). All are operator-visible account emails / public flow metadata — never credentials. Old dashboards ignore unknown fields; old peers simply lack `paneAlive` (→ tri-state unknown downstream).
59
+ - Timing dependence: pane capture at read time (races documented in §2, all fail safe). No conversation-state dependence.
60
+
61
+ ## 6b. Operator-surface quality
62
+
63
+ This change IS an operator-surface-quality fix (the whole point of the case study). Against the four questions: (1) **leads with the primary action** — every actionable cell leads with its one button (Set up / Sign in / Retry), and the in-flight cell leads with the step instruction + the Sign in link; the PIN stage adds an explicit Back so the operator is never trapped. (2) **Zero raw internals as primary content** — accounts render by EMAIL, machines by NICKNAME (a raw `m_<hex>` id is now suppressed rather than shown — the D5 wording floor, with `friendlyMachine` unit-tested); the only values the operator ever types are their PIN and the provider's own sign-in code (both existing patterns, memory-only, cleared after use); no JSON, fingerprints, or ids are shown or asked for. (3) **Destructive actions de-emphasized** — Cancel is a small secondary control under the flow, guarded by a native confirm; Back is client-side-only and destroys nothing. (4) **Plain language at phone width** — all new copy is jargon-free sentences ("The sign-in page must show X — if it shows a different account, tap 'Switch account' first"; "That code signed in X — this slot needs Y"), the failure/expiry/success states are color+glyph+sentence (not codes), and the flow stays inside one grid cell so the phone never needs the bottom of the page. Error copy only references affordances that exist on the surface it appears on (the "re-tap Approve" ghost-button wording is gone).
64
+
65
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
66
+
67
+ Proxied-on-read, matching the existing WS5.2 design: the pool-scope pending-logins fan-out inherits each peer's OWN `paneAlive`/nickname annotation (each machine answers for its own tmux — liveness is machine-local BY NATURE and never guessed cross-machine); the submit-code/cancel relays are unchanged; a LEGACY peer (pre-this-version) returns rows without `paneAlive` → rendered as the unknown state (submittable, exactly today's behavior) — mixed-version pools degrade to current behavior, never to a fabricated verdict. The enroll/start single-attempt check runs on the TARGET machine (the only place its pane exists). Client transients/episodes are per-dashboard-session by design (another dashboard sees server-truth states). URLs surfaced are the provider's own OAuth URLs — machine-boundary safe.
68
+
69
+ ## 8. Rollback cost
70
+
71
+ Low. No config/schema/migration surface; no state format change (pending-login records unchanged — `paneAlive` is computed at read time, never stored). Revert = revert the PR. The one behavioral write-path change (upsert-on-validated, supersede-dead-pane abandon) only produces states the system already supports (active account; abandoned login). A hot-fix can also disable nothing selectively because nothing ships dark — this is a defect fix to an already-dev-gated feature (`multiMachine.accountFollowMe` gates every touched route; fleet installs keep 503ing exactly as before).
72
+
73
+ ---
74
+
75
+ **Second-pass response:** not required (no new blocking authority; the touched gates keep their exact refusal sets — see Decision-point inventory).
76
+
77
+ ## Follow-up (same PR): no-silent-fallbacks ratchet exemption
78
+
79
+ The D5 pane-liveness guard's `captureOutput` catch (`src/server/routes.ts`, follow-me submit-code route) sets `rawFrame = null` on capture failure, which the ratchet's `hasStateReset` heuristic counted as a new silent fallback (493 > 492 baseline, Unit shard 3/4 red). It is a refusal path, not a degradation — null flows to the explicit `pane-dead` 409 refusal (fails closed; the code is never blind-typed). Marked `@silent-fallback-ok` in place on the same line (no detection-window reshaping of neighboring catch blocks). Side effects: none — comment-only; behavior byte-identical.
@@ -0,0 +1,31 @@
1
+ # Subscriptions "Set up" flow fixes — plain-English overview
2
+
3
+ ## What this actually is
4
+
5
+ The dashboard's Subscriptions tab has a grid — accounts down the side, your machines across the top — where tapping "Set up" on an empty cell signs an account in on that machine: enter your PIN, open a sign-in link, paste back a code. Justin walked this flow on his phone on 2026-07-10 and hit five real problems, each screenshot-proven. This change fixes all five.
6
+
7
+ 1. **The page stopped fighting your fingers.** The tab refreshes itself every 30 seconds, and that refresh used to REBUILD the grid — wiping the PIN box back into a "Set up" button if you didn't type fast enough, and swapping the paste-your-code step for a spinner before you could paste. Now there's a rule (Floor F9 of the Dashboard UX Standard): a refresh may never replace anything you're in the middle of using. An open step, a focused box, or a half-typed field stays exactly where it is; the refresh only updates things AROUND it, like the "link expires in 11m" countdown. The hold releases when the step finishes, fails, expires, or you tap Back.
8
+
9
+ 2. **The whole flow lives in the cell now.** Before, the sign-in link and code box appeared in the cell, but the expiry countdown and the "Claude may ask for TWO codes" heads-up only existed in a panel at the very bottom of the page — Justin thought the flow had stalled until he happened to scroll. Now the cell carries every step end to end; the bottom panel is just a mirror.
10
+
11
+ 3. **The wrong-account trap is guarded.** The sign-in link opens Anthropic's page in whatever account your browser is already logged into — Justin was enrolling headley.justin@gmail.com and the page said "Logged in as justin@sagemindai.io", with no warning anywhere. Now the cell says, right next to the link: "the sign-in page must show headley.justin@gmail.com — if it shows a different account, tap Switch account first." And the server-side identity check (which already existed and already refused mismatches) now tells you exactly what happened in plain words: "that code signed in justin@sagemindai.io — this slot needs headley.justin@gmail.com. The account was NOT enrolled." If the identity can't be confirmed at all, it still refuses — it never enrolls blind.
12
+
13
+ 4. **Success looks like success.** Finishing used to be a quiet grey line of text. Now the cell flips to a green "✓ All set — [account] is signed in", pulses briefly, and the panel keeps an explicit "✓ Done" card. Failures and expired links get equally explicit red states with a working Retry.
14
+
15
+ 5. **The re-sign-in path actually works now.** Justin's "Needs sign-in → Sign in" attempt died three ways at once: the record said "signing in" while the window doing the sign-in was long gone (so the code he pasted had nowhere to go), a second tap started a PARALLEL attempt whose code crossed with the first, and finishing would have crashed anyway because the account already existed in the registry. Now the server checks that the sign-in window is genuinely alive before offering a code box (a dead one shows "Sign-in needs a restart" with a Retry that starts cleanly), a re-tap always returns the ONE live attempt instead of minting a rival, a sign-in that completes without ever showing a code is detected and finished through the same identity check, and re-signing-in an existing account updates it back to active instead of crashing. Wording is fixed too: you see account emails and machine nicknames ("Laptop"), never internal IDs, and no error tells you to tap a button that doesn't exist.
16
+
17
+ ## What already existed
18
+
19
+ The whole enrollment machinery (PIN gate, mandate, sign-in driving, the fail-closed email-identity check, the Cancel button on in-progress cells) already shipped and stays exactly as it was. This change fixes how the flow is PRESENTED and makes the record-keeping honest — the one behavioral addition is the liveness check + single-attempt rule + completion sweep around the existing pieces.
20
+
21
+ ## The safeguards, in plain terms
22
+
23
+ - The identity check that refuses wrong accounts is unchanged — it still refuses, it just explains itself now.
24
+ - Nothing new is auto-accepted anywhere; every new refusal (dead window, not-ready window) refuses things that already failed before, just honestly.
25
+ - If the system can't tell whether a sign-in window is alive, it says "unknown" and behaves exactly like today — it never guesses.
26
+
27
+ ## What you need to decide
28
+
29
+ Nothing — these are defect fixes to an existing flow, on by wherever the account-follow-me feature is already on (your dev agents; dark on the fleet). The one follow-up left open: rolling the new never-clobber refresh rule out to the OTHER dashboard tabs (Mandates, Process Health, Preferences) is tracked in the standard rather than crammed into this fix.
30
+
31
+ _Follow-up (same PR): the pane-liveness check's "couldn't look at the window" handler is deliberately conservative — it treats an unreadable window as dead and refuses the code rather than typing blind. A code-quality checker mistook that refusal for a silent failure; it now carries a note explaining itself. No behavior change._