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
GPU's negotiated family enum, and the nine family-gate rows
(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.**
`ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**.
- **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
rerun green. Same budget as the occurrence above, now seen in two
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
failures** (composition budget in `04-lib-crdt`, then composition
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
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)
- **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
/// ownership.
///

View File

@ -728,6 +728,13 @@ impl SemanticRenderState {
/// per §5b's split table, because the release it denies does not
/// exist on this branch.
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);
}