fix(panel): a second press ends the gesture it replaces

Found by reading the arming path back after the G5 checkpoint, not by a
failing row. `arm_accepted_gesture` overwrote an already-armed latch, so
a dropped `Up` --- one lost to an outbox that closed under a stall ---
was followed by the next press silently discarding the first gesture's
record, without counting it as a cancellation.

Inert on this base, where records are only counted. Once
`panel-pointer-replay` attaches a child release to each record, the
discarded one leaves a button held down with nothing left to release
it, and the arming code that decides this is this slice's.

Mutation: restore the plain overwrite -> the new row alone.

Also records three CI-red observations from the slice-completion gate,
which is the first entry on this lane with a MEASURED confound instead
of the standing uncontrolled one. Three wall-clock-deadline rows red in
one run --- criterion_1 by 0.12%, a PTY lifecycle race, and a 5s child
-exit deadline --- all green in isolation, the last in 0.15s against
its 5s deadline. `uptime` during the run went 14.02 -> 28.35, from an
unrelated turso test suite on the same machine with one binary at 693%
CPU. Not a controlled experiment, but the same evidence U9's synthetic
-load control was meant to produce, and it points at load.

Two process traps are recorded with them, because both were made here.
The Bash tool caps a command at ten minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running and
which reads exactly like a real failure. And `pkill -f <pattern>` kills
the invoking shell when the pattern appears in its own command line, so
the intended target survives while the operator believes it died --- and
here `pkill -f "cargo test"` would have destroyed an unrelated
project's build. Identify by PID.

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; the four §5b G5 rows. The full protocol gate follows on
a quieter machine; the run described above is not evidence for this tree
and is recorded as an observation only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-16 08:45:41 +02:00
parent 419b078c0f
commit bf4ffe0995
No known key found for this signature in database
4 changed files with 137 additions and 1 deletions

View File

@ -287,7 +287,12 @@ from #171 and #215.
half (G1–G4), outbound family selection, inbound family gating, the half (G1–G4), outbound family selection, inbound family gating, the
GPU's negotiated family enum, and the nine family-gate rows GPU's negotiated family enum, and the nine family-gate rows
(G6a–b, G7a–b, G8). **Full `--protocol` gate green at `3c06176`.** (G6a–b, G7a–b, G8). **Full `--protocol` gate green at `3c06176`.**
In progress since, uncommitted: G5's accepted-gesture latch. Then three more: G5a's trigger and its latch, the wheel exemption
with exhaustion and mapped coalescing, and the receiver rows.
**Every row this slice owns is now witnessed** — G0, G1–G4a, G5a plus
latch substrate, G6–G8, G9a–c, G10/G10a–c, G11a, G13a–b, G14a–b, G15
— with G4b, G5b–e/g/i–p, G6c, G7c, G11b and G12a–b left to the replay
lane per §5b's split table.
- **PROTOCOL-BEARING — v25, and it runs alone.** - **PROTOCOL-BEARING — v25, and it runs alone.**
`ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**. `ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**.
- **Why it exists.** A `PanelPointer` names a cell and nothing on the - **Why it exists.** A `PanelPointer` names a cell and nothing on the
@ -465,6 +470,35 @@ from #171 and #215.
(2026-08-15, plain `cargo test --lib`, not a gate step). Isolated (2026-08-15, plain `cargo test --lib`, not a gate step). Isolated
rerun green. Same budget as the occurrence above, now seen in two rerun green. Same budget as the occurrence above, now seen in two
different selectors on one branch in one day. different selectors on one branch in one day.
- **Three wall-clock-deadline rows red in ONE run, under a MEASURED
foreign load** (2026-08-16, log `20260816T063330Z`). Steps
`04-lib-crdt` and `09-sweep-crdt`. Fragments:
`criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke`
— "per-keystroke orchestrator time **1.001196ms** exceeds 1ms",
**0.12% over**, the smallest margin any budget on this lane has
failed by; `m6_1_pty_mode_lifecycle_started_then_exited` — "must
observe stdout 'done'"; and
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` — "child
did not exit within 5s". All three green in isolation, the last in
**0.15s against its 5s deadline**, a 33x margin.
- **This is the first entry on this lane with a NAMED confound rather
than the standing uncontrolled one.** `uptime` during the run:
load average **14.02 → 28.35**, from an unrelated `turso` test
suite on the same machine (`./verify_task_state.sh
turso-without-rowid`, target dir `/opt/target`, one test binary at
**693% CPU**). It is not a controlled experiment, but it is the
same evidence U9's synthetic-load control was meant to produce, and
it points at load. U9 stays owed; its value is now lower.
- **Two process traps this cost, both worth carrying forward.** The
Bash tool caps a command at 10 minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running —
indistinguishable from a real failure in the summary line. Run the
protocol gate under `setsid` and watch the log. And `pkill -f
<pattern>` kills the invoking shell when the pattern appears in its
own command line, so the intended target survives and the operator
believes it died. **Both mistakes were made here**; the second
nearly killed an unrelated project's build, because
`pkill -f "cargo test"` would have matched it. Identify by PID.
- **Three consecutive full-gate runs, three DIFFERENT unrelated - **Three consecutive full-gate runs, three DIFFERENT unrelated
failures** (composition budget in `04-lib-crdt`, then composition failures** (composition budget in `04-lib-crdt`, then composition
again in `10-sweep-crdt`, then this in `08-sweep`), against a diff again in `10-sweep-crdt`, then this in `08-sweep`), against a diff

View File

@ -2497,6 +2497,47 @@ Two consequences are recorded rather than fixed:
later and leaving that one out would be an inconsistency inside a later and leaving that one out would be an inconsistency inside a
single function rather than a clean deferral. single function rather than a clean deferral.
#### What the slice actually landed, row by row
Recorded because the split table above states the *intent*, and a
reader auditing this later needs the *outcome* — including the two
places where writing the witnesses changed the design.
**Landed here:** G0 (wire shapes, positions, field order, accumulated
pins), G1–G4a (the authoritative key, document and terminal halves),
G5a plus the latch substrate, G6a–b/G7a–b/G8a–e (both directions, all
four wrong-family quadrants, forged identity), G9a/G9b/G9c, G10 and
G10a–c, G11a, G13a–b, G14a–b, G15.
**Still the replay lane's**, unchanged: G4b, G5b–e, G5g, G5i–p, G6c,
G7c, G11b, G12a–b.
Two corrections the mutation pass forced, both of which had passed
their first review as written:
- **G10a proved nothing about zero.** Asserted with a generation
already held, zero is also *lower* than it, so the nondecreasing
clause did the refusing and deleting the zero check left the row
green. Zero is only isolable before any authority exists — which is
also the case the framing names, a sender that never initialised the
field. Split into its own row with that setup.
- **G11a's exhaustion latch had no proven job.** The overflow path
already returns before storing the ceiling snapshot, so the next read
re-takes the changed arm and the band stays down without any latch.
Measured, the two are alternatives: only removing both resurrects the
band. The latch is kept, and now earns its place through `peek`,
which honours it so the peek and the authoritative read agree that an
exhausted session has no key rather than reporting the ceiling.
One witness-shape note, since it recurs: the two G10b rows call
`panel_mapping_is_current` directly. A wheel has **no
dispatcher-visible effect on this base** — a document panel focuses on
`Down` only, and no panel pointer coordinate is consumed anywhere — so
a row asserting focus for a wheel would be green whatever the gate did.
Each row carries a press leg alongside, which does have an effect, to
show the predicate is wired into the production arm rather than merely
correct in isolation.
### Coherence impact (`COHERENCE.md` §20) ### Coherence impact (`COHERENCE.md` §20)
- **Journey steps touched: 5** (document-panel editing and selection) - **Journey steps touched: 5** (document-panel editing and selection)

View File

@ -7189,6 +7189,60 @@ mod tests {
); );
} }
/// §5b — SUBSTRATE: a second accepted press ENDS the gesture it
/// replaces rather than overwriting it.
///
/// Found by reading the arming path back, not by a failing row: a
/// dropped `Up` — one lost to a closed outbox under a stall — is
/// followed by the next press, and a plain overwrite discards the
/// first gesture's record without counting it. Inert on this base,
/// where records are only counted; once replay attaches a child
/// release to each record, the discarded one leaves a button held
/// down with nothing left to release it.
#[test]
fn g5_substrate_a_second_press_ends_the_gesture_it_replaces() {
let fid = FrontendId(777);
let (mut editor, mut semantic_states, mut render, _document, panel, epochs) =
panel_session_at(PROTOCOL_VERSION, fid);
let buffer_id = editor.core.borrow().windows[&panel].buffer_id;
let generation = stamped_generation(&semantic_states, fid);
let press = || mapped_pointer(fid, epochs, buffer_id, generation);
dispatch_panel_event(
&mut editor,
fid,
PROTOCOL_VERSION,
&mut semantic_states,
&mut render,
press(),
);
assert!(semantic_states[&fid].has_accepted_gesture());
assert_eq!(
semantic_states[&fid].panel_gesture_cancellations(),
0,
"fixture: the first press cancels nothing"
);
// The `Up` never arrives; the next press does.
dispatch_panel_event(
&mut editor,
fid,
PROTOCOL_VERSION,
&mut semantic_states,
&mut render,
press(),
);
assert!(
semantic_states[&fid].has_accepted_gesture(),
"the new gesture is armed"
);
assert_eq!(
semantic_states[&fid].panel_gesture_cancellations(),
1,
"and the one it displaced was ENDED, not dropped on the floor"
);
}
/// §5b — SUBSTRATE for the framing's G5p: per-frontend gesture /// §5b — SUBSTRATE for the framing's G5p: per-frontend gesture
/// ownership. /// ownership.
/// ///

View File

@ -728,6 +728,13 @@ impl SemanticRenderState {
/// per §5b's split table, because the release it denies does not /// per §5b's split table, because the release it denies does not
/// exist on this branch. /// exist on this branch.
pub fn arm_accepted_gesture(&mut self, gesture: AcceptedPanelGesture) { pub fn arm_accepted_gesture(&mut self, gesture: AcceptedPanelGesture) {
// A second accepted press while one is already armed means the
// first gesture's release never arrived — a dropped `Up`, or an
// outbox that closed under a stall. END it rather than
// overwrite it: overwriting discards the record silently, and
// once replay attaches effects to that record the child is left
// holding a button down with nothing left to release it.
self.cancel_accepted_gesture();
self.accepted_gesture = Some(gesture); self.accepted_gesture = Some(gesture);
} }