From ab7c2079041a0f37d9ae657afa76f99b469350e3 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 28 Jul 2026 14:08:17 -0400 Subject: [PATCH] Keep the v21 panel wire dark for v20 clients Reserve the additive v21 panel schema without advertising it in the server-first production handshake. Pin a real shipped-v20 client attach, make the two aggregate-budget ratchets exactly one byte over, and update the framing, coherence audit, handoff, and volatile lane record. --- COHERENCE.md | 10 +- docs/active-work.md | 48 +++++-- docs/agent-handoff.md | 52 +++++--- docs/bottom-panel-stage2-framing.md | 126 +++++++++++++----- pmacs-protocol/src/lib.rs | 23 ++-- pmacs-protocol/src/message.rs | 24 +++- pmacs-protocol/src/terminal.rs | 18 ++- src/daemon.rs | 11 +- ...ottom_panel_stage2b_protocol_acceptance.rs | 80 ++++++++++- tests/common/daemon.rs | 4 +- tests/gpu_invocation_acceptance.rs | 12 +- tests/m5_5_acceptance.rs | 22 +-- tests/m5_7_acceptance.rs | 6 +- tests/m5_perf_acceptance.rs | 8 +- tests/mode_system_wiring_acceptance.rs | 8 +- tests/vterm_stage3_acceptance.rs | 4 +- 16 files changed, 325 insertions(+), 131 deletions(-) diff --git a/COHERENCE.md b/COHERENCE.md index 758b3e2..e997614 100644 --- a/COHERENCE.md +++ b/COHERENCE.md @@ -109,7 +109,7 @@ remain open to them. | 13 | Package lifecycle UX | **Resolution without lifecycle** | Mature resolver/lockfile; init-only install; no uninstall/disable/search | | 14 | Workbench primitives | **Partial (best trajectory)** | Listview is a real shared primitive; bottom panel landed (#155) | | 15 | Contextual affordances | **Weak** | Right-click menu only; code actions apply first-blindly; no git integration at all | -| 16 | Semantic frontend | **Strong** | v6..=v20 negotiated protocol; degradation practiced; TUI/GPU share the model | +| 16 | Semantic frontend | **Strong** | v6..=v21 schema support; production attach remains v20 during the dark panel slice; degradation practiced | | 17 | Distribution | **Missing** | CI is test-only; no binaries, channels, checksums, or update path | | 18 | Onboarding | **Missing** | No welcome, no tutorial; `C-h` deletes a word; `M-x` is the only door in | | 19 | Coherence acceptance tests | **Started** | `tests/journey_acceptance.rs` exists (steps 2, 3, 5); the other five scenarios are still unwritten | @@ -1340,9 +1340,13 @@ facto privileged implementation. **Grade: strong — the healthiest concern in this document, and most of its asks are already practiced.** -- Versioned, negotiated protocol `SUPPORTED=[6..=20]` with deliberate +- Versioned protocol schema `SUPPORTED=[6..=21]` with deliberate encoding-breaking bumps, both-frontends support required per bump, - and byte-pin discipline for appended variants (handoff §4). + and byte-pin discipline for appended variants (handoff §4). The v21 + bottom-panel family is reserved but dark in Stage 2B-1: because + `Hello` is server-first, the production daemon still advertises v20 + so shipped v20 clients remain attachable; compatible v21 activation + belongs to Stage 2B-3. - Two genuine frontends share the conceptual model; CRDT concurrent editing with presence across them; remote attach + reconnect. - **Graceful per-frontend degradation is practiced, not aspirational**: diff --git a/docs/active-work.md b/docs/active-work.md index b419173..837cd56 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -305,13 +305,15 @@ If it does not, stop and repair the remote/fetch configuration. never been enforced. Any CI job that compiles the `crdt` targets has to fix them first or it will be red on arrival. -## Bottom-panel lane (Arc 7) — 2B-1 GATED; PR #184 OPEN FOR REVIEW +## Bottom-panel lane (Arc 7) — 2B-1 REVIEW FIX IN PROGRESS; PR #184 OPEN Stage 1, the Stage 2 framing, and Stage 2A are on `main`. Framing revision 5's three-way split of 2B was explicitly approved on -2026-07-27. **Stage 2B-1 is implemented, integrated with canonical -`main` @ `7fd646d`, and fully gated at `c8895a8`; it has no remaining -dependency. PR #184 is open and must not merge before user review.** +2026-07-27; revision 6 records PR #184's review correction. **Stage +2B-1 is implemented and integrated with canonical `main` @ `7fd646d`. +The previous head was fully gated at `c8895a8`, but review round 2 found +four issues and the corrected head must run the full gate again. PR +#184 is open and must not merge before user review.** - **Stage 2B-1 branch:** `bottom-panel-stage2b`, based on `githubsucks/main` @ `7fd646d` by merge because review had begun. @@ -320,14 +322,27 @@ dependency. PR #184 is open and must not merge before user review.** and gate checkpoint is committed and pushed; nothing depends on a worktree or `/tmp`. PR #184: . -- **Ships only the v21 wire layer:** the four wire shapes, version bump, - shared cell-grid validator, and version-ladder move. It has no - producer, consumer, or capability change; `panel_capable` stays - `false`, so this slice changes no user-visible journey grade. +- **Ships only the reserved v21 wire layer:** the four wire shapes, + schema version, shared cell-grid validator, and accepted-version + ladder move. The production daemon continues advertising v20 because + its `Hello` is server-first; v21 activation belongs to 2B-3. This + slice has no producer, consumer, or capability change; + `panel_capable` stays `false`, so it changes no user-visible journey + grade and existing v20 clients remain attachable. - **Review round 1 closed:** two P1s and one P2, all corrected at `9b364ad`: `PanelFrame` now identifies its buffer, the transport ratchet covers the actual attach path rather than a detached codec assertion, and shared grid bounds have one validator. +- **Review round 2 found four issues; fixes are in progress:** the + server-first `Hello` made the advertised v20↔v21 compatibility + one-way; `COHERENCE.md` and `docs/agent-handoff.md` still named only + v20 schema support; framing §9 named a nonexistent aggregate 2B + suite instead of the three exact 2B slice suites; and the panel plus + copied terminal "one byte over" fixtures were actually two bytes + over. The correction keeps production advertisement at v20, adds a + real-daemon existing-v20-client acceptance, updates all three durable + records, names the exact slice suites, and asserts both rejecting + fixtures are exactly `limit + 1`. - **The full gate found and corrected two 2B-1 omissions:** the statusline version ladder still pinned v20/rejected v21, and Vterm Stage 3 pinned v20 both structurally and in its real headless probe. @@ -350,7 +365,7 @@ dependency. PR #184 is open and must not merge before user review.** `pmacs --gpu .` path, consumed the asynchronous dired snapshot, and retained the managed daemon before the wait so failure cleanup remains effective. The code integration auto-composed. -- **The complete post-integration gate is green at `c8895a8`:** +- **The previous complete post-integration gate was green at `c8895a8`:** formatting; strict workspace Clippy; library **1,849 passed + 3 ignored default** and **2,034 passed + 4 ignored CRDT**; bottom-panel Stage 1 / 2A / 2B-1 **46 / 17 / 15**; folding Stage 2 **48**; GPU font @@ -438,8 +453,9 @@ dependency. PR #184 is open and must not merge before user review.** `docs/agent-handoff.md` §1; the two round lessons are in §5. - Landed-docs follow-up merged as **#156** (`main` @ `d152120`, 2026-07-25). -- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5** - is commit `56301ed` on branch `githubsucks/bottom-panel-stage2b`, +- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 6** + is on branch `githubsucks/bottom-panel-stage2b` (revision 5 is commit + `56301ed` there), worktree `../pmacs-bp-stage2b`. Revisions 1–4 remain on `githubsucks/bottom-panel-stage2-framing` (head `4fbd47f`, four framing commits, revision 4 at `49757e5`). Round 1 closed 2 blocking + @@ -447,7 +463,9 @@ dependency. PR #184 is open and must not merge before user review.** round 2 closed 1 blocking + 2 high + 1 medium and decided both open items; round 3 closed 1 blocking + 1 high + 1 medium. No open items remain. Revision 5 adds no decision; it records the approved - 2B-1/2B-2/2B-3 implementation split. The + 2B-1/2B-2/2B-3 implementation split. Revision 6 corrects the + server-first compatibility contract, durable protocol claims, exact + acceptance-suite names, and `limit + 1` fixture. The parent framing `docs/bottom-panel-framing.md` (rev 4) remains authoritative, **including its acceptance criteria 37–55**. - Retained, carrying nothing unmerged: branch `bottom-panel` and worktree @@ -456,12 +474,14 @@ dependency. PR #184 is open and must not merge before user review.** before the next branches: **2A** = classified §1.3 census routing + `paint_frame` per-window painter extraction (with the active-window auto-scroll preparation), no - protocol change; **2B-1** = protocol **v21** + protocol change; **2B-1** = reserved protocol schema **v21**, with + production advertisement held at v20, (`InstanceMessage::PanelFrame` plus `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`, gated both directions, each extended enum byte-pinned on its own previous final variant); **2B-2** = daemon panel projection and epoch - machine; **2B-3** = the GPU band and negotiated `panel_capable` flip. + machine; **2B-3** = compatible v21 activation, the GPU band, and the + negotiated `panel_capable` flip. Stage 3 is the adopter default flip. - **Correction — this entry previously mis-stated the census contract.** It is **not** "route every consumer through `primary_document_window`". diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index d2e8ee2..bc6f440 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,7 +1,9 @@ # Agent handoff — cross-machine continuity -**Last updated: 2026-07-28, after the Journey/GPU directory-target -ratchet (#183), following Journey Stage 1a (#182), which made directory +**Last updated: 2026-07-28, during bottom-panel Stage 2B-1 PR #184 +review; the canonical landed base remains the Journey/GPU +directory-target ratchet (#183), following Journey Stage 1a (#182), +which made directory startup one coherent local/daemon/GPU path and incorporated the terminal configuration + copy mode landed-doc work (#180); following terminal copy mode (#178) — `C-c C-t` @@ -58,9 +60,11 @@ commands, read `docs/active-work.md` immediately after this file. #165, the GPU terminal input fix #166, Lean 4 Stage 2 #161, the dired framing #164, COHERENCE.md #163, find-file #162, Lean 4 Stage 1 #160, minimap blank-slab #159, bottom-panel Stage 1 #155). Protocol unchanged - at **v20** — bottom-panel Stage 2A deliberately carries no wire change; - v21 arrives with Stage 2B. The bullets below describe the arcs in their - own terms; this line is the head-of-`main` anchor. + at **v20** — bottom-panel Stage 2A deliberately carries no wire change. + The in-review Stage 2B-1 reserves the v21 schema but keeps the + server-first production `Hello` at v20; compatible activation belongs + to Stage 2B-3. The bullets below describe the arcs in their own terms; + this line is the head-of-`main` anchor. - **`COHERENCE.md` is now required reading and a required framing input — #163.** It carries the product-coherence thesis, an audited scorecard, per-concern gaps, and §20's priority order, and it is the @@ -395,9 +399,14 @@ commands, read `docs/active-work.md` immediately after this file. unchanged, which is the additivity gate for the `read_dir` change; M4 121; required GPU 155; isolated-`XDG_CONFIG_HOME` workspace sweep 3,205 across 93 suites. 15 claims bite-verified. -- Protocol **v20** (`SUPPORTED=[6..=20]`; v16 = `ThemeFacts`, v17 = - `FontFacts`, v18 = `StatuslineSegments`, v19 = terminal frames/events, v20 = - the GPU initial-target semantic bootstrap family). +- Canonical `main` is protocol **v20** (`SUPPORTED=[6..=20]`; v16 = + `ThemeFacts`, v17 = `FontFacts`, v18 = `StatuslineSegments`, v19 = + terminal frames/events, v20 = the GPU initial-target semantic + bootstrap family). Bottom-panel Stage 2B-1's in-review schema is v21 + (`SUPPORTED=[6..=21]`), but its production daemon deliberately + advertises v20: the handshake is server-first, so advertising 21 + would make shipped v20 GPU/TUI clients reject before + `AttachRequest`. Stage 2B-3 owns compatible production activation. - **Bottom panel Stage 1 (window placement + TUI side windows) LANDED — #155** (`docs/bottom-panel-framing.md` rev 4; merge `e745068`; two review rounds). **No protocol change (still v20).** Arc 7's substrate: pmacs now @@ -463,14 +472,18 @@ commands, read `docs/active-work.md` immediately after this file. required GPU 152; initial-target 14 CRDT; all three vterm suites; folding Stage 2 48. All 12 CI checks green at merge. - **Stage 2 (the GPU panel band) is FRAMED** — - `docs/bottom-panel-stage2-framing.md` rev 5, four review rounds, no - open items; the rev-5 implementation split was explicitly approved - 2026-07-27. It takes protocol **v21** and ships as four serial + `docs/bottom-panel-stage2-framing.md` rev 6, four framing review + rounds, no open framing items; the rev-5 implementation split was + explicitly approved 2026-07-27 and rev 6 records PR #184's + server-first compatibility and gate correction. It reserves + protocol **v21** and ships as four serial implementation slices: **2A** classified census routing + per-window painter extraction (no wire change), **2B-1** the wire, **2B-2** the daemon projection and epoch machine, then **2B-3** the - GPU band and negotiated `panel_capable` flip. Parent acceptance - 37–55 remains authoritative. Stage 3 is the adopter default flip. + GPU band, compatible v21 activation, and negotiated + `panel_capable` flip. Production attachment remains v20 through + 2B-1 and 2B-2. Parent acceptance 37–55 remains authoritative. + Stage 3 is the adopter default flip. - **The §1.3 census is CLASSIFIED, not uniformly redirected.** Only the Projection class (#1–#12, #21–#22) routes through `primary_document_window`; focus/input (#13–#15, #23), focus chrome @@ -1201,12 +1214,17 @@ buffer owns a path's recovery slot; only recover/discard release unclaimed crash data; adopt clears the old owner's skip cache. **Protocol** — encoding-breaking bumps are deliberate and versioned. Canonical -`main` is `[6..=20]`. v15 = `CompletionPopup` + `StatusFacts.message`; v16 = +`main` is `[6..=20]`. The in-review bottom-panel 2B-1 schema extends support +to `[6..=21]`, while `ADVERTISED_PROTOCOL_VERSION` stays 20 until 2B-3 +provides compatibility-preserving activation; the server-first `Hello` +cannot advertise 21 without stranding existing v20 clients before +`AttachRequest`. v15 = `CompletionPopup` + `StatusFacts.message`; v16 = `ThemeFacts`; v17 = `FontFacts`; v18 = `StatuslineSegments`; v19 = the vterm terminal family; v20 = semantic `SessionBootstrapRequest` plus appended -`InitialTargetResult`. New wire surface ⇒ bump + both-frontends support + -acceptance. An APPENDED variant must be guarded by a byte pin on the PREVIOUS -final variant — its own round-trip cannot detect a discriminant shift. +`InitialTargetResult`; v21 reserves the panel frame/event family. New wire +surface ⇒ bump + both-frontends support + acceptance. An APPENDED variant +must be guarded by a byte pin on the PREVIOUS final variant — its own +round-trip cannot detect a discriminant shift. **Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`, `rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation), diff --git a/docs/bottom-panel-stage2-framing.md b/docs/bottom-panel-stage2-framing.md index 28dd68b..2384118 100644 --- a/docs/bottom-panel-stage2-framing.md +++ b/docs/bottom-panel-stage2-framing.md @@ -1,10 +1,13 @@ # Bottom panel Stage 2 — the GPU panel band (framing) -**Revision 5 — APPROVED 2026-07-27; 2A merged, 2B-1 in progress. -Ground truth: canonical `main` @ `c2d56ff`, protocol v20 on `main` and -v21 on `bottom-panel-stage2b`.** Revisions 1–4 were -pre-implementation; rev 5 records the three-way slice of Stage 2B -(§0.0, §7.2, §9) after its first slice was already built. +**Revision 6 — PR #184 review correction; the underlying Stage 2 +framing remains APPROVED 2026-07-27. 2A is merged and 2B-1 is under +review. Ground truth: canonical `main` @ `7fd646d`, protocol v20 on +`main`; `bottom-panel-stage2b` reserves the v21 schema while its +server-first production handshake continues to advertise v20.** +Revisions 1–4 were pre-implementation; rev 5 recorded the three-way +slice of Stage 2B after its first slice was already built; rev 6 +corrects that slice's mixed-version and gate contracts. Stage 1 (#155, merge `e745068`) gave pmacs window placement, window parameters, TUI side windows, the divider, and the adopter `display` @@ -29,7 +32,33 @@ geometries), Q#BP16 (pointer transport), Q#BP17 (fold projection), and ## 0. Revision history -### 0.0 Rev 4 → rev 5 — the three-way slice of 2B (not a review round) +### 0.0 Rev 5 → rev 6 — PR #184 review round 2, four findings closed + +- **R6-1 (P1) — v21 is reserved, not advertised, in 2B-1.** The + protocol's handshake is server-first. An existing v20 TUI or GPU + frontend rejects a `Hello { protocol_version: 21 }` before it can + send an `AttachRequest`, so rev 5's claim that a v21 daemon and v20 + peer "still negotiate 20" was impossible. 2B-1 therefore extends the + schema and accepted-version ladder to v21 while the production daemon + continues advertising v20. A real-daemon acceptance emulates the + shipped v20 rejection point and then requires the attachment to reach + its initial grid. **2B-3 owns both a compatibility-preserving + activation mechanism and the production move to v21; it may not + simply change the unsolicited `Hello` to 21.** +- **R6-2 (P2) — durable protocol claims move with the wire.** + `COHERENCE.md` and `docs/agent-handoff.md` now distinguish v21 schema + support from the still-v20 production handshake. +- **R6-3 (P2) — the gate contract names the actual decomposition.** + §9 now names 2B-1's + `bottom_panel_stage2b_protocol_acceptance` suite and the exact planned + daemon/GPU suite names for 2B-2 and 2B-3 instead of the nonexistent + `bottom_panel_stage2b_acceptance`. +- **R6-4 (P2) — "one byte over" means exactly one.** The panel and + copied terminal boundary fixtures replace a one-byte cluster with a + two-byte cluster, and each independently asserts a total of + `limit + 1`. + +### 0.1 Rev 4 → rev 5 — the three-way slice of 2B (not a review round) This revision changes no decision. It splits one approved implementation slice into three and reallocates the acceptance @@ -68,18 +97,17 @@ criteria across them. 2 and 3 are written. - **R5-4 — two of the three slices ship dark, deliberately.** Nothing in 2B-1 or 2B-2 is reachable by a user: `panel_capable` stays - `false` for every negotiated semantic session until 2B-3, so a v21 - daemon and a v21 GPU frontend negotiate 21 and behave exactly as - they do at v20. This is the same posture 2A took ("seam adoption - that becomes load-bearing in 2B") and it carries the same - obligation: **the version bump advertises a capability whose only - distinguishing feature is unreachable until 2B-3 lands.** That is - safe for compatibility — the variants are appended, the ladder is - extended, and a v20 peer still negotiates 20 — but it means the arc - must not stall between 2B-1 and 2B-3. Recorded here so a stall is - visible as a decision rather than inherited as a default. + `false` for every negotiated semantic session until 2B-3. Rev 5 + incorrectly described that posture as a production v21 negotiation + that remained compatible with v20 clients; rev 6 R6-1 supersedes + that claim. The actual dark posture keeps the server-first + production handshake on v20 while the v21 schema is reserved. This + is the same posture 2A took ("seam adoption that becomes + load-bearing in 2B"), and it means the arc must not stall between + 2B-1 and 2B-3. Recorded here so a stall is visible as a decision + rather than inherited as a default. -### 0.1 Round 3 (rev 3 → rev 4) — 1 blocking, 1 high, 1 medium, all closed +### 0.2 Round 3 (rev 3 → rev 4) — 1 blocking, 1 high, 1 medium, all closed - **R3-1 (blocker).** Rev 3's three-boundary model was right but its call-site table was wrong in five places, and each error was a real @@ -107,7 +135,7 @@ criteria across them. `cell.attachment.is_some()` rejection. It is now classified — and **shared**, with the reasoning pinned. -### 0.2 Round 2 (rev 2 → rev 3) — 1 blocking, 2 high, 1 medium, all closed +### 0.3 Round 2 (rev 2 → rev 3) — 1 blocking, 2 high, 1 medium, all closed - **R2-1 (blocker).** Rev 2's "one document-bottom seam" conflated two boundaries that must **diverge** once a panel exists. Several sites it @@ -134,7 +162,7 @@ criteria across them. - Both §8 open items are decided (§5.3): `BASE_DIVIDER_HEIGHT = 4.0` at scale 1.0, and `TEXT_TOP` stays unscaled. -### 0.3 Round 1 (rev 1 → rev 2) — 2 blocking, 3 high, 3 revision points, all closed +### 0.4 Round 1 (rev 1 → rev 2) — 2 blocking, 3 high, 3 revision points, all closed - **R1-1 (blocker).** Rev 1 said all 23 census reads route through `primary_document_window`. That contradicts Q#BP14, which routes only @@ -181,9 +209,10 @@ criteria across them. | Byte pin `InstanceMessage::InitialTargetResult` | `pmacs-protocol/src/message.rs:1145` | Holds — still the enum's final variant | | Byte pin `FrontendEvent::TerminalPointer` | final variant of its enum | Holds | -**Protocol is still v20** (`pmacs-protocol/src/message.rs:1568`); no -intervening PR bumped it. Q#BP9's conditional resolves: **Stage 2 is -v21**, no reservation was taken and none was needed. +**Protocol was still v20 at this re-scout**; no intervening PR had +bumped it. Q#BP9's conditional resolved: **Stage 2 reserves v21**. +Rev 6 R6-1 adds the server-first compatibility constraint discovered +during 2B-1 review. Fifteen PRs merged between the parent's last re-scout (`47581f4`) and this one: #149, #150, #152–#155, #158–#166. Nothing in the parent's @@ -375,13 +404,18 @@ undedicated (Q#BP2c). "It receives no new events" is insufficient — if the daemon nevertheless places that frontend's window in a side panel it cannot render, the window becomes invisible. The gate is on placement, not only on transport. Parent acceptance 51 pins the mixed -session. +session. **The production daemon does not advertise v21 in 2B-1 or +2B-2.** Because `Hello` is server-first, 2B-3 must add or prove a +compatibility-preserving way to activate v21 before applying this rule; +merely advertising 21 would strand already-shipped v20 clients before +they can identify themselves. ## 4. Revisions to the parent framing Only these; everything else stands. -- **Q#BP9 resolves to v21.** +- **Q#BP9 resolves to the v21 schema, with production advertisement + held at v20 until 2B-3 supplies compatible activation.** - **Q#BP15a's epoch ownership is specified** by §3.1's table and API split, replacing the parent's one-line "frontend-owned" statement. - **Q#BP8's statusline criterion splits** per §3.3: one read reroutes, @@ -741,7 +775,10 @@ consumer, no capability change. becomes 21, `SUPPORTED_PROTOCOL_VERSIONS` accepts `6..=21` and rejects 22, and any test whose *name* encodes the old number is renamed. A ladder pin that passes across a bump was not pinning the - version. + version. **`ADVERTISED_PROTOCOL_VERSION` remains 20 in 2B-1 and + 2B-2** because the unsolicited `Hello` precedes any client version + signal. A real daemon must remain attachable by a client whose + supported range ends at 20. - **Shared bounds are aliased, not duplicated.** Every constant the terminal screen and the panel validator both enforce is one definition with the other as an alias, so truncation and validation @@ -790,9 +827,11 @@ production negotiation until 2B-3. #### 7.2.3 Slice 2B-3 — the GPU band and the capability flip -**Authority: `pmacs-gpu`, plus the negotiation rule.** This is the only -slice a user can observe, and the only one that closes the journey -divergence in §6. +**Authority: `pmacs-gpu`, plus the compatibility-preserving negotiation +activation.** This is the only slice a user can observe, and the only +one that closes the journey divergence in §6. It must not advertise +v21 in the server-first `Hello` until an existing v20 client can still +attach. - **46** (band + divider shrink the document text area by exactly their pixel height; carets, hits, and scroll geometry respect the reduced @@ -838,7 +877,10 @@ divergence in §6. its chrome. - **A2B-5.** `panel_capable` is true only for a v21+ negotiated authenticated semantic session; a v20 semantic session is never - **placed** in a side window, not merely denied the events. + **placed** in a side window, not merely denied the events. The same + acceptance must attach an actual v20 client to the production daemon + after v21 activation, so the new path cannot pass by breaking the old + handshake before placement is evaluated. ## 8. Open items @@ -866,17 +908,20 @@ stacked, and each is cut from `main`. installed. - **Stage 2B-1 — the v21 wire layer.** Branch `bottom-panel-stage2b`. The four wire shapes, the version bump, the shared cell-grid - validator, and the version-ladder move. **No producer, no consumer, - no capability change** — `panel_capable` stays `false`. + validator, and the version-ladder move. The v21 schema is reserved + while the production daemon continues advertising v20. **No + producer, no consumer, no capability change** — `panel_capable` + stays `false`. - **Stage 2B-2 — the daemon panel projection and epoch machine.** Cut from `main` after 2B-1 merges. Produces `PanelFrame` and owns stale-event rejection, exercised through a **test-only** panel-capable semantic view. Still no production flip. - **Stage 2B-3 — the GPU band and the negotiated flip.** Cut from `main` after 2B-2 merges. The three-boundary text-area split, the - divider, pointer routing, and `panel_capable = true` for a v21+ - negotiated authenticated semantic session. **This is the slice that - changes what a user sees**, and it repeats 2A's and 2B-2's relevant + divider, pointer routing, the compatibility-preserving v21 + activation, and `panel_capable = true` for a v21+ negotiated + authenticated semantic session. **This is the slice that changes + what a user sees**, and it repeats 2A's and 2B-2's relevant assertions through the real capability flip. **Each slice runs the full gate set below, not a subset of it.** A @@ -889,9 +934,16 @@ Gates for each slice: the standing suite from `CLAUDE.md`, plus the **touched acceptance suites named explicitly** — the standing rule is to run the suites a change touches, and "standing suite" does not name them: -- `bottom_panel_stage1_acceptance` — the substrate both slices build on. -- `bottom_panel_stage2a_acceptance` / `bottom_panel_stage2b_acceptance` - — new, one per slice. +- `bottom_panel_stage1_acceptance` — the substrate all four Stage 2 + slices build on. +- `bottom_panel_stage2a_acceptance` — Stage 2A's classified census and + painter extraction. +- `bottom_panel_stage2b_protocol_acceptance` — Stage 2B-1's v21 schema, + server-first v20 compatibility, byte pins, and shared validation. +- `bottom_panel_stage2b_daemon_acceptance` — the exact suite name + reserved for Stage 2B-2's projection and epoch machine. +- `bottom_panel_stage2b_gpu_acceptance` — the exact suite name reserved + for Stage 2B-3's band, compatible activation, and capability flip. - `statusline_segments_acceptance` — the fan-out target change (§3.3). - `m11_5_semantic_acceptance` — the semantic census (§3.2). - `gpu_initial_target_acceptance` — parent criterion 55. diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs index 82cdd5b..e7e36e3 100644 --- a/pmacs-protocol/src/lib.rs +++ b/pmacs-protocol/src/lib.rs @@ -57,17 +57,18 @@ pub use cell::{ pub use crdt::CrdtOp; pub use ids::{BufferId, ByteRange, FrontendId, Position}; pub use message::{ - AdornmentContent, AdornmentPlacement, AttachRequest, BUILTIN_PAIR_CHARS, BlockAdornment, - CompletionPopupRow, CursorState, Decoration, DecorationKind, DecorationSegment, - FrontendCapabilities, FrontendEvent, GoodbyeReason, Hello, InitialTarget, InitialTargetResult, - InlineAdornment, InstanceCapabilities, InstanceIdentity, InstanceMessage, InstanceSignal, Key, - KeyEvent, LineNumberMode, MAX_INITIAL_TARGET_ERROR_BYTES, MAX_INITIAL_TARGET_PATH_BYTES, - MAX_STATUSLINE_FACE_BYTES, MAX_STATUSLINE_PROVIDER_NAME_BYTES, MAX_STATUSLINE_PROVIDERS, - MAX_STATUSLINE_SEGMENT_BYTES, MAX_STATUSLINE_TOTAL_TEXT_BYTES, MenuPromptRow, Modifiers, - MouseButton, MouseEvent, MouseKind, NegotiatedCapabilities, PROTOCOL_VERSION, PointerKind, - ResourceBody, SUPPORTED_PROTOCOL_VERSIONS, SelectionSnapshot, SessionBootstrapRequest, - StatuslineSegment, StyleSegment, StyleSpan, ThemeFace, is_builtin_pair_char, - is_modeline_face_name, is_supported_protocol_version, is_ui_face_name, negotiate_capabilities, + ADVERTISED_PROTOCOL_VERSION, AdornmentContent, AdornmentPlacement, AttachRequest, + BUILTIN_PAIR_CHARS, BlockAdornment, CompletionPopupRow, CursorState, Decoration, + DecorationKind, DecorationSegment, FrontendCapabilities, FrontendEvent, GoodbyeReason, Hello, + InitialTarget, InitialTargetResult, InlineAdornment, InstanceCapabilities, InstanceIdentity, + InstanceMessage, InstanceSignal, Key, KeyEvent, LineNumberMode, MAX_INITIAL_TARGET_ERROR_BYTES, + MAX_INITIAL_TARGET_PATH_BYTES, MAX_STATUSLINE_FACE_BYTES, MAX_STATUSLINE_PROVIDER_NAME_BYTES, + MAX_STATUSLINE_PROVIDERS, MAX_STATUSLINE_SEGMENT_BYTES, MAX_STATUSLINE_TOTAL_TEXT_BYTES, + MenuPromptRow, Modifiers, MouseButton, MouseEvent, MouseKind, NegotiatedCapabilities, + PROTOCOL_VERSION, PointerKind, ResourceBody, SUPPORTED_PROTOCOL_VERSIONS, SelectionSnapshot, + SessionBootstrapRequest, StatuslineSegment, StyleSegment, StyleSpan, ThemeFace, + is_builtin_pair_char, is_modeline_face_name, is_supported_protocol_version, is_ui_face_name, + negotiate_capabilities, }; pub use panel::{MAX_PANEL_VISIBLE_CELLS, PanelFrame, PanelFrameError, PanelFramePayload}; pub use terminal::{ diff --git a/pmacs-protocol/src/message.rs b/pmacs-protocol/src/message.rs index e3795a3..0e4e815 100644 --- a/pmacs-protocol/src/message.rs +++ b/pmacs-protocol/src/message.rs @@ -1665,6 +1665,16 @@ pub enum ResourceBody { /// window invisible. pub const PROTOCOL_VERSION: u32 = 21; +/// Protocol version placed in the daemon's server-first [`Hello`]. +/// +/// Bottom-panel Stage 2B-1 reserves the additive v21 wire family, but +/// production attachment remains on v20 until the Stage 2B-3 capability +/// activation can preserve compatibility with existing v20 frontends. +/// Those frontends reject an unknown server-first version before they can +/// send [`AttachRequest`], so advertising [`PROTOCOL_VERSION`] here would +/// make the otherwise-dark protocol slice user-visible. +pub const ADVERTISED_PROTOCOL_VERSION: u32 = 20; + /// T M10.5: the set of protocol versions a v1.0 binary accepts on /// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept /// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat @@ -1742,9 +1752,12 @@ pub const PROTOCOL_VERSION: u32 = 21; /// sessions send a bounded bootstrap envelope after `AttachRequest`; legacy /// and non-semantic sessions retain their existing handshake shape. /// -/// Bottom panel Stage 2 (Q#BP9): extended to `[6, ..., 21]`. v21 peers -/// may exchange panel traffic; v20 peers interoperate with it simply -/// absent, and are never placed in a side window. +/// Bottom panel Stage 2 (Q#BP9): extended to `[6, ..., 21]`. Stage 2B-1 +/// reserves and validates the v21 wire while production daemons continue +/// to send [`ADVERTISED_PROTOCOL_VERSION`] in their server-first +/// [`Hello`]. The later capability-activation slice owns moving production +/// negotiation to v21 without making existing v20 frontends reject the +/// handshake. pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; @@ -2126,7 +2139,10 @@ pub fn negotiate_capabilities( /// frontend will use as the `FrontendId` on every event it sends. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Hello { - /// The instance's `PROTOCOL_VERSION`. + /// The protocol version this attachment should use. + /// + /// This can deliberately trail [`PROTOCOL_VERSION`] while an additive + /// wire family is reserved but not yet activated in production. pub protocol_version: u32, /// `FrontendId` assigned to this attachment by the instance. The /// frontend stamps this onto subsequent events. v0.1 daemons start diff --git a/pmacs-protocol/src/terminal.rs b/pmacs-protocol/src/terminal.rs index 8f7f507..0bcf7e0 100644 --- a/pmacs-protocol/src/terminal.rs +++ b/pmacs-protocol/src/terminal.rs @@ -843,14 +843,14 @@ mod tests { let mut over = exact.clone(); // One more byte of glyph, nothing else changed. let last = over.cells.len() - 1; - over.cells[last] = cell_with(Glyph::Cluster(cluster_of_len(3).into_boxed_slice())); + over.cells[last] = cell_with(Glyph::Cluster(cluster_of_len(2).into_boxed_slice())); (exact, over) } #[test] fn maximum_legal_terminal_frame_encodes_below_the_transport_cap() { - let (exact, _) = budget_boundary_frames(); + let (exact, over) = budget_boundary_frames(); assert_eq!(exact.validate(), Ok(())); let mut glyph_bytes = 0usize; @@ -865,6 +865,20 @@ mod tests { glyph_bytes, MAX_TERMINAL_FRAME_GLYPH_BYTES, "the measured fixture must spend the whole aggregate budget" ); + let over_glyph_bytes = over + .cells + .iter() + .map(|cell| match &cell.glyph { + Glyph::Char(ch) => ch.len_utf8(), + Glyph::Cluster(bytes) => bytes.len(), + Glyph::Continuation => 0, + }) + .sum::(); + assert_eq!( + over_glyph_bytes, + MAX_TERMINAL_FRAME_GLYPH_BYTES + 1, + "the rejecting twin must be exactly one byte over the aggregate budget" + ); let msg = InstanceMessage::TerminalFrame(exact); let bytes = postcard::to_allocvec(&msg).expect("encode"); diff --git a/src/daemon.rs b/src/daemon.rs index 43c97ad..b665d07 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -67,9 +67,9 @@ use crate::lockfile::{self, LockError, LockHandle}; use crate::presence::{PresenceSnapshot, SessionRegistry}; use crate::protocol::crossterm_translate::{key_to_crossterm, mouse_to_crossterm}; use crate::protocol::{ - AttachRequest, FrontendEvent, FrontendId, GoodbyeReason, Hello, InitialTarget, - InitialTargetResult, InstanceCapabilities, InstanceIdentity, InstanceMessage, InstanceSignal, - MAX_INITIAL_TARGET_ERROR_BYTES, MAX_INITIAL_TARGET_PATH_BYTES, PROTOCOL_VERSION, PointerKind, + ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendEvent, FrontendId, GoodbyeReason, Hello, + InitialTarget, InitialTargetResult, InstanceCapabilities, InstanceIdentity, InstanceMessage, + InstanceSignal, MAX_INITIAL_TARGET_ERROR_BYTES, MAX_INITIAL_TARGET_PATH_BYTES, PointerKind, SelectionSnapshot, SessionBootstrapRequest, }; use crate::socket_path::{SocketPathError, ensure_runtime_subdir}; @@ -712,7 +712,7 @@ fn per_attach_thread( // mismatch path without changing the default. let instance_caps_for_hello = instance_capabilities_with_env_override(); let hello = Hello { - protocol_version: PROTOCOL_VERSION, + protocol_version: ADVERTISED_PROTOCOL_VERSION, assigned_frontend_id: frontend_id, instance_identity: daemon_state.build_identity(), instance_capabilities: instance_caps_for_hello.clone(), @@ -739,7 +739,7 @@ fn per_attach_thread( let _ = write_message( &mut stream, &InstanceMessage::Goodbye(GoodbyeReason::VersionMismatch { - server: PROTOCOL_VERSION, + server: ADVERTISED_PROTOCOL_VERSION, client: req.protocol_version, }), ); @@ -3412,6 +3412,7 @@ fn apply_event( #[cfg(test)] mod tests { use super::*; + use crate::protocol::PROTOCOL_VERSION; #[test] fn daemon_state_starts_frontend_id_at_two() { diff --git a/tests/bottom_panel_stage2b_protocol_acceptance.rs b/tests/bottom_panel_stage2b_protocol_acceptance.rs index c3ec239..b066e4a 100644 --- a/tests/bottom_panel_stage2b_protocol_acceptance.rs +++ b/tests/bottom_panel_stage2b_protocol_acceptance.rs @@ -5,17 +5,28 @@ //! projection, the epoch state machine, and the GPU band are later //! slices of this stage and are not exercised here. +mod common; + +use std::time::Duration; + use pmacs_protocol::cell::{Cell, CellCoord, CellSize, Color, Glyph, Style, UnderlineStyle}; -use pmacs_protocol::message::{FrontendEvent, InstanceMessage, Modifiers, MouseButton, MouseKind}; +use pmacs_protocol::message::{ + AttachRequest, FrontendEvent, Hello, InstanceMessage, Modifiers, MouseButton, MouseKind, +}; use pmacs_protocol::panel::{ MAX_PANEL_VISIBLE_CELLS, PanelFrame, PanelFrameError, PanelFramePayload, }; use pmacs_protocol::terminal::{ MAX_TERMINAL_COLS, TerminalFrame, TerminalFrameError, TerminalProcessState, }; -use pmacs_protocol::transport::MAX_FRAME_BYTES; +use pmacs_protocol::transport::{MAX_FRAME_BYTES, read_message, write_message}; use pmacs_protocol::wire_grid::{MAX_WIRE_GRID_GLYPH_BYTES, MAX_WIRE_GRID_GRAPHEME_BYTES}; -use pmacs_protocol::{BufferId, FrontendId, PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS}; +use pmacs_protocol::{ + ADVERTISED_PROTOCOL_VERSION, BufferId, FrontendId, PROTOCOL_VERSION, + SUPPORTED_PROTOCOL_VERSIONS, +}; + +use common::daemon::{TestDaemon, build_default_caps}; fn cell(ch: char) -> Cell { Cell { @@ -98,11 +109,54 @@ fn terminal_frame(rows: u32, cols: u32) -> TerminalFrame { fn the_panel_stage_takes_protocol_v21() { assert_eq!(PROTOCOL_VERSION, 21); assert!(SUPPORTED_PROTOCOL_VERSIONS.contains(&21)); - // v20 stays supported: a v20 peer interoperates with panel traffic - // simply absent rather than being refused the handshake. + // The wire family is reserved before it is activated: the production + // server-first Hello must remain acceptable to already-shipped v20 + // clients throughout the dark protocol and daemon slices. + assert_eq!(ADVERTISED_PROTOCOL_VERSION, 20); assert!(SUPPORTED_PROTOCOL_VERSIONS.contains(&20)); } +#[test] +fn a_new_daemon_keeps_an_existing_v20_client_attachable() { + let daemon = TestDaemon::spawn(); + let mut stream = daemon.connect(); + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .expect("set handshake timeout"); + + // This is the rejection point in an already-shipped client: it reads the + // daemon's unsolicited Hello before it is able to identify its own + // supported range or send AttachRequest. + let hello: Hello = read_message(&mut stream).expect("read daemon Hello"); + let v20_client_supported_versions = 6..=20; + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); + assert!( + v20_client_supported_versions.contains(&hello.protocol_version), + "an existing v20 client would reject the server-first Hello" + ); + + write_message( + &mut stream, + &AttachRequest { + protocol_version: hello.protocol_version, + frontend_capabilities: build_default_caps(), + initial_size: CellSize::new(24, 80), + }, + ) + .expect("write v20 AttachRequest"); + + assert!( + matches!( + read_message::(&mut stream).expect("read initial grid"), + InstanceMessage::CellDelta { + full_grid: true, + .. + } + ), + "the daemon must establish the v20 session, not merely send an acceptable Hello" + ); +} + #[test] fn a_present_panel_frame_round_trips_with_both_epochs() { let frame = panel_frame(2, 3); @@ -483,7 +537,7 @@ fn panel_budget_boundary_frames() -> (PanelFrame, PanelFrame) { let mut over = exact.clone(); // One more byte of glyph, nothing else changed. let last = over.cells.len() - 1; - over.cells[last] = maximal_cell(Glyph::Cluster(cluster_of_len(3).into_boxed_slice())); + over.cells[last] = maximal_cell(Glyph::Cluster(cluster_of_len(2).into_boxed_slice())); (exact, over) } @@ -508,6 +562,20 @@ fn maximum_legal_panel_frame_encodes_below_the_transport_cap() { glyph_bytes, MAX_WIRE_GRID_GLYPH_BYTES, "the measured fixture must spend the whole aggregate budget" ); + let over_glyph_bytes = over + .cells + .iter() + .map(|cell| match &cell.glyph { + Glyph::Char(ch) => ch.len_utf8(), + Glyph::Cluster(bytes) => bytes.len(), + Glyph::Continuation => 0, + }) + .sum::(); + assert_eq!( + over_glyph_bytes, + MAX_WIRE_GRID_GLYPH_BYTES + 1, + "the rejecting twin must be exactly one byte over the aggregate budget" + ); // One byte over is rejected, which is what makes `exact` maximal. assert!(matches!( diff --git a/tests/common/daemon.rs b/tests/common/daemon.rs index 6cb087a..1c8bc0d 100644 --- a/tests/common/daemon.rs +++ b/tests/common/daemon.rs @@ -26,7 +26,7 @@ use tempfile::TempDir; #[cfg(feature = "crdt")] use pmacs::cell::CellSize; #[cfg(feature = "crdt")] -use pmacs::protocol::{AttachRequest, PROTOCOL_VERSION}; +use pmacs::protocol::AttachRequest; use pmacs::protocol::{FrontendCapabilities, Hello}; use pmacs::transport::read_message; #[cfg(feature = "crdt")] @@ -294,7 +294,7 @@ pub fn attach_multi(daemon: &TestDaemon) -> (Hello, UnixStream) { .unwrap(); let hello: Hello = read_message(&mut stream).expect("read Hello"); let req = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: multi_frontend_caps(), initial_size: CellSize::new(24, 80), }; diff --git a/tests/gpu_invocation_acceptance.rs b/tests/gpu_invocation_acceptance.rs index b4e3102..2f56e8f 100644 --- a/tests/gpu_invocation_acceptance.rs +++ b/tests/gpu_invocation_acceptance.rs @@ -93,9 +93,9 @@ mod crdt { use pmacs::cell::CellSize; use pmacs::crdt::CrdtState; use pmacs::protocol::{ - AttachRequest, FrontendCapabilities, FrontendEvent, FrontendId, Hello, InitialTarget, - InitialTargetResult, InstanceCapabilities, InstanceIdentity, InstanceMessage, - PROTOCOL_VERSION, SessionBootstrapRequest, + ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendCapabilities, FrontendEvent, + FrontendId, Hello, InitialTarget, InitialTargetResult, InstanceCapabilities, + InstanceIdentity, InstanceMessage, PROTOCOL_VERSION, SessionBootstrapRequest, }; use pmacs::transport::{read_message, write_message}; @@ -244,11 +244,11 @@ mod crdt { .set_read_timeout(Some(Duration::from_secs(5))) .expect("set target frontend timeout"); let hello: Hello = read_message(&mut stream).expect("target frontend Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); write_message( &mut stream, &AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: FrontendCapabilities { multi_frontend: true, crdt_replica: true, @@ -583,7 +583,7 @@ mod crdt { facts .get("server_protocol_version") .and_then(|value| value.parse::().ok()), - Some(PROTOCOL_VERSION) + Some(ADVERTISED_PROTOCOL_VERSION) ); assert_eq!( facts.get("spawned_daemon").map(String::as_str), diff --git a/tests/m5_5_acceptance.rs b/tests/m5_5_acceptance.rs index ee66ea8..c8c87ed 100644 --- a/tests/m5_5_acceptance.rs +++ b/tests/m5_5_acceptance.rs @@ -37,8 +37,8 @@ use pmacs::cell::Color; #[cfg(feature = "crdt")] use pmacs::overlay_color::color_for_slot; use pmacs::protocol::{ - AttachRequest, FrontendCapabilities, FrontendEvent, GoodbyeReason, Hello, InstanceMessage, Key, - KeyEvent, Modifiers, PROTOCOL_VERSION, + ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendCapabilities, FrontendEvent, GoodbyeReason, + Hello, InstanceMessage, Key, KeyEvent, Modifiers, PROTOCOL_VERSION, }; use pmacs::transport::{read_message, write_message}; @@ -52,9 +52,9 @@ use common::daemon::{ /// Read the daemon's `Hello`, send our `AttachRequest`, return the Hello. fn do_handshake(stream: &mut UnixStream) -> Hello { let hello: Hello = read_message(stream).expect("read Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); let req = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: build_default_caps(), initial_size: CellSize::new(24, 80), }; @@ -418,7 +418,7 @@ fn version_mismatch_clean_disconnect() { // Read Hello. let hello: Hello = read_message(&mut stream).expect("Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); // Send AttachRequest with wrong protocol version. let req = AttachRequest { @@ -431,7 +431,7 @@ fn version_mismatch_clean_disconnect() { // Expect Goodbye(VersionMismatch). match read_message::(&mut stream) { Ok(InstanceMessage::Goodbye(GoodbyeReason::VersionMismatch { server, client })) => { - assert_eq!(server, PROTOCOL_VERSION); + assert_eq!(server, ADVERTISED_PROTOCOL_VERSION); assert_eq!(client, 999); } other => panic!("expected VersionMismatch Goodbye, got {other:?}"), @@ -1145,9 +1145,9 @@ fn m10_10_production_attach_negotiates_crdt_replica() { // Production handshake — NOT the test `attach_multi()` path. let hello: Hello = read_message(&mut stream).expect("read Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); let req = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: pmacs::attach::build_capabilities(), initial_size: CellSize::new(24, 80), }; @@ -1183,9 +1183,9 @@ fn m10_10_production_attach_non_crdt_build_does_not_negotiate_crdt_replica() { stream .set_read_timeout(Some(Duration::from_secs(5))) .unwrap(); - let _hello: Hello = read_message(&mut stream).expect("read Hello"); + let hello: Hello = read_message(&mut stream).expect("read Hello"); let req = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: pmacs::attach::build_capabilities(), initial_size: CellSize::new(24, 80), }; @@ -2171,7 +2171,7 @@ fn m10_10_f14_production_path_keystroke_flows_to_broadcast() { .unwrap(); let hello_a: Hello = read_message(&mut stream_a).expect("A Hello"); let req_a = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello_a.protocol_version, frontend_capabilities: pmacs::attach::build_capabilities(), initial_size: CellSize::new(24, 80), }; diff --git a/tests/m5_7_acceptance.rs b/tests/m5_7_acceptance.rs index ec0733d..ce85591 100644 --- a/tests/m5_7_acceptance.rs +++ b/tests/m5_7_acceptance.rs @@ -77,7 +77,7 @@ use nix::unistd::Pid; use tempfile::TempDir; use pmacs::attach::PMACS_TEST_SSH_BIN; -use pmacs::protocol::{Hello, PROTOCOL_VERSION}; +use pmacs::protocol::{ADVERTISED_PROTOCOL_VERSION, Hello}; use pmacs::transport::read_message; // --------------------------------------------------------------------------- @@ -385,7 +385,7 @@ fn daemon_attach_bridges_hello_from_existing_daemon() { // verbatim. (No AttachRequest sent — the daemon will hold the // attach slot until the bridge stdin closes below.) let hello: Hello = read_message(&mut bridge_stdout).expect("read Hello via bridge"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); // Tear down: drop bridge stdin → bridge's stdin→socket copy sees // EOF, shuts down the socket write half, the daemon notices and @@ -424,7 +424,7 @@ fn daemon_attach_auto_starts_missing_daemon() { // bound the socket and the bridge connected. let hello: Hello = read_message(&mut bridge_stdout).expect("read Hello via auto-started daemon"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); // The lockfile must exist now: `acquire_lock` writes it on // daemon startup. (Existence of the lockfile is what proves diff --git a/tests/m5_perf_acceptance.rs b/tests/m5_perf_acceptance.rs index 4d1a680..12139b3 100644 --- a/tests/m5_perf_acceptance.rs +++ b/tests/m5_perf_acceptance.rs @@ -72,8 +72,8 @@ use tempfile::TempDir; use pmacs::cell::CellSize; use pmacs::protocol::{ - AttachRequest, FrontendCapabilities, FrontendEvent, Hello, InstanceMessage, Key, KeyEvent, - Modifiers, PROTOCOL_VERSION, + ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendCapabilities, FrontendEvent, Hello, + InstanceMessage, Key, KeyEvent, Modifiers, }; use pmacs::transport::{TransportError, read_message, write_message}; @@ -158,9 +158,9 @@ fn build_default_caps() -> FrontendCapabilities { fn do_handshake(stream: &mut UnixStream) -> Hello { let hello: Hello = read_message(stream).expect("read Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); let req = AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: build_default_caps(), initial_size: CellSize::new(24, 80), }; diff --git a/tests/mode_system_wiring_acceptance.rs b/tests/mode_system_wiring_acceptance.rs index bd79b4e..16b6fe5 100644 --- a/tests/mode_system_wiring_acceptance.rs +++ b/tests/mode_system_wiring_acceptance.rs @@ -11,8 +11,8 @@ use std::time::{Duration, Instant}; use pmacs::cell::{Cell, CellSize, Glyph}; use pmacs::protocol::{ - AttachRequest, FrontendEvent, FrontendId, Hello, InstanceMessage, Key, KeyEvent, Modifiers, - PROTOCOL_VERSION, + ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendEvent, FrontendId, Hello, InstanceMessage, + Key, KeyEvent, Modifiers, }; use pmacs::transport::{read_message, write_message}; @@ -75,11 +75,11 @@ fn attach(daemon: &TestDaemon) -> (Client, Grid) { .set_read_timeout(Some(Duration::from_secs(5))) .expect("set daemon handshake timeout"); let hello: Hello = read_message(&mut stream).expect("read daemon Hello"); - assert_eq!(hello.protocol_version, PROTOCOL_VERSION); + assert_eq!(hello.protocol_version, ADVERTISED_PROTOCOL_VERSION); write_message( &mut stream, &AttachRequest { - protocol_version: PROTOCOL_VERSION, + protocol_version: hello.protocol_version, frontend_capabilities: build_default_caps(), initial_size: CellSize::new(ROWS, COLS), }, diff --git a/tests/vterm_stage3_acceptance.rs b/tests/vterm_stage3_acceptance.rs index f729aba..9656491 100644 --- a/tests/vterm_stage3_acceptance.rs +++ b/tests/vterm_stage3_acceptance.rs @@ -716,8 +716,8 @@ fn a37_real_daemon_real_pty_and_headless_gpu_render_one_terminal_session() { assert_eq!( facts.get("server_protocol_version").copied(), - Some("21"), - "the real daemon negotiated v21 with the real client: {text}" + Some("20"), + "the dark v21 wire slice must keep the real client on v20: {text}" ); assert_eq!( facts.get("entered_terminal_mode").copied(),