14ffe9c4
docs: record the selection lane's totality and history watermark
a73x 2026-08-18 12:27
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -3752,3 +3752,81 @@ while its native CLI copy-mode scope remains open and separate. `7c777ec6` | |||
| 3752 | maps to the OSC 52 browser clipboard path, and `ee062dd9` to the shared mode | 3752 | maps to the OSC 52 browser clipboard path, and `ee062dd9` to the shared mode |
| 3753 | semantics and DEC 2004 browser paste path; the mouse-mirroring half of the | 3753 | semantics and DEC 2004 browser paste path; the mouse-mirroring half of the |
| 3754 | latter remains separate. No issue is closed by this record. | 3754 | latter remains separate. No issue is closed by this record. |
| 3755 | |||
| 3756 | ## 2026-08-18 (selection review: totality, the history watermark, intent) | ||
| 3757 | |||
| 3758 | **A request the daemon could decode leaves with an answer.** The selection | ||
| 3759 | lane is request/reply, and the only thing a requester can do with silence is | ||
| 3760 | wait out its timeout. So the decode now happens BEFORE the session lookup, | ||
| 3761 | and the two orderings mean different things: a malformed payload carries no | ||
| 3762 | id to correlate, and silence is the only honest response to it — but a | ||
| 3763 | well-formed request from a slot with no session (promoted by a QUIC | ||
| 3764 | handshake, never attached) does carry one, and answering `unavailable` costs | ||
| 3765 | a frame where dropping it cost the client five seconds. The same rule | ||
| 3766 | governs the encode: `unavailable` with no text is the one reply that cannot | ||
| 3767 | fail validation, so a failed encode of anything else falls back to it, and | ||
| 3768 | the capacity that fallback needs is reserved before the attempt that can | ||
| 3769 | fail. Totality is a property of the lane, not a property of the happy path. | ||
| 3770 | |||
| 3771 | **Absolute screen rows are not a name for a line, so the reply carries the | ||
| 3772 | watermark that says so.** Row zero is the OLDEST RETAINED row, not the | ||
| 3773 | oldest row the session ever had. When the page list evicts a page every | ||
| 3774 | absolute row shifts underneath a request already in flight, and the reply | ||
| 3775 | that comes back is `ok`, valid UTF-8, and text the user never highlighted — | ||
| 3776 | the worst shape a bug can take, because nothing about it looks wrong. | ||
| 3777 | `selection_reply` therefore carries the daemon's `history_rows` sampled at | ||
| 3778 | extraction time, and the requester compares it with the reading it took when | ||
| 3779 | its own request became authoritative. | ||
| 3780 | |||
| 3781 | The comparison is deliberately `lower means discard`, not `different means | ||
| 3782 | discard`, and that is the whole subtlety. Ordinary output RAISES the retained | ||
| 3783 | count without renaming a single row, because new rows are appended below the | ||
| 3784 | ones already there; demanding equality would make every selection taken | ||
| 3785 | during output unusable, which is most of the selections anyone wants. Only | ||
| 3786 | eviction lowers it. Measured on a five-column grid: the count climbed to | ||
| 3787 | 10000, dropped to 5214 in one step, and absolute row zero changed identity at | ||
| 3788 | exactly that drop — page-sized jumps, never a slow drift. | ||
| 3789 | |||
| 3790 | What this buys is a watermark, not a lease, and the difference is the point: | ||
| 3791 | the daemon holds no per-client selection state and gains none here. The | ||
| 3792 | residual hole is that an eviction followed by a whole page of fresh output | ||
| 3793 | INSIDE one round trip would read as a rise. That needs thousands of rows | ||
| 3794 | between the client's last applied frame and the extraction, and closing it | ||
| 3795 | would mean the daemon remembering who asked for what — a trade this feature | ||
| 3796 | has refused from the start. | ||
| 3797 | |||
| 3798 | **User intent beats arrival order on the clipboard.** An explicit copy is the | ||
| 3799 | newest statement of what the human wants, so it retires the automatic OSC 52 | ||
| 3800 | lane rather than racing it. A write already handed to the browser genuinely | ||
| 3801 | cannot be cancelled and that limit still stands — but the re-dispatch such a | ||
| 3802 | write performs when it settles is ours to start, and starting it put terminal | ||
| 3803 | text on the system clipboard after the gesture that asked for something else, | ||
| 3804 | with the UI still reading "Copied" for the selection. Retiring the version and | ||
| 3805 | the queued value together is what suppresses it. The rule is about which | ||
| 3806 | writes we choose to begin, and it does not pretend to own the ones already | ||
| 3807 | in the browser's hands. | ||
| 3808 | |||
| 3809 | **The browser honours one OSC 52 target.** The protocol still accepts every | ||
| 3810 | Pc xterm defines, because the native client can route them: `p` is PRIMARY, | ||
| 3811 | `s` the selection, `0`-`7` the cut buffers. A browser tile has one | ||
| 3812 | destination and no way to reach the others, and writing them all to the | ||
| 3813 | system clipboard meant that an ordinary mouse drag inside the session — which | ||
| 3814 | is exactly what emits `ESC]52;p;…` in many X11 applications — silently | ||
| 3815 | clobbered whatever the human last copied on their own machine. Narrowing the | ||
| 3816 | web path to `c` is a loss of nothing anybody asked for. Non-`c` targets | ||
| 3817 | remain valid on the wire and are simply not this tile's business. | ||
| 3818 | |||
| 3819 | **Validation run for this round.** `make build` and `make test` both exit 0, | ||
| 3820 | and `web/verify.js` reports 704/704 against `zig-out/bin/mux_core.wasm`, run | ||
| 3821 | directly as well as through the build graph. Every new assertion was watched | ||
| 3822 | failing first, against the mutation or the bug it exists for. | ||
| 3823 | |||
| 3824 | The cross-version gate ran too, containerised, with this tree against | ||
| 3825 | v0.0.1-5 in both directions: 9 passed, 0 failed, 1 skipped, the skip being | ||
| 3826 | the agent surface the old ref predates. What it measures about the wire | ||
| 3827 | change is the general property and not the specific frame — an old client | ||
| 3828 | ignores daemon-to-client frames it has no arm for and keeps parsing past | ||
| 3829 | them, because they are length-prefixed and its dispatch ends in | ||
| 3830 | `else => {}`. It could not be made to measure `selection_reply` itself, | ||
| 3831 | and that is a fact about the protocol rather than a gap in the rig: nothing | ||
| 3832 | can make a daemon send one to a client that never asks. | ||