The attach-mode optimistic-apply layer (M10.10) classifies any
plain-char keystroke as `Insert(c)` and applies it directly to the
local CRDT mirror, bypassing the daemon's keymap dispatcher. The
documented limitation ("the optimistic layer doesn't track keymap-
prefix state") also covered the minibuffer-active case, which
surfaced during session-5 manual validation: characters typed into a
`C-x C-f` prompt were optimistically inserted into the previously-
active document instead of routed to the minibuffer.
The fix is a daemon→frontend wire signal indicating whether the
daemon's *next* key event would be intercepted (minibuffer or pending
prefix) vs would self-insert. The frontend gates the optimistic-apply
path on this; when not idle, every keystroke round-trips as
`FrontendEvent::Key`.
Protocol changes (pmacs-protocol):
- `PROTOCOL_VERSION` 3 → 4; `SUPPORTED_PROTOCOL_VERSIONS` adds 4.
- New `InstanceMessage::DispatchIdle { idle: bool }`.
Daemon (`src/editor.rs`, `src/daemon.rs`):
- `EditorState::dispatch_idle()` — true iff `dispatcher.pending`
empty AND `minibuffer.is_active() == false`.
- Per-tick emission: `last_dispatch_idle_sent: HashMap<FrontendId,
bool>` tracks the last-broadcast value per session; emission fires
on first frame after attach (absent entry) and on transitions.
- Gated on `crdt_replica` AND `negotiated_protocol_version >= 4` so
older peers don't hard-error on the unknown variant. Same gating
shape as the M10.5 CrdtOp and M11.1 SemanticFrame bumps.
Frontend (`src/attach.rs`):
- New `dispatch_idle: bool` (cfg `crdt`); default `false`
(pessimistic — optimistic apply only activates after the daemon
explicitly says idle).
- DispatchIdle messages consumed in the drain loop; they don't
participate in `present_messages` batches.
- Optimistic-apply branch gated on `dispatch_idle`. When false, the
branch returns false (forces fallthrough to the round-trip
`forward_event` path).
Tests:
- `editor::tests::dispatch_idle_*` — fresh, prefix-pending, prefix-
resolved, minibuffer-open/cancelled.
- `protocol::tests::dispatch_idle_round_trips_through_postcard` —
wire encoding both polarities.
- `protocol::tests::protocol_version_is_four_for_dispatch_idle` +
`supported_protocol_versions_includes_one_through_four` — pin the
new version constants.
Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1474 (+5 from 1469 baseline) with crdt; 1312 (+4) without;
m4 83; m11_5 (--features crdt) 2.
Acknowledged remaining gap: plain-char Lua bindings (e.g. binding
`q` to a command) still surface optimistic-apply divergence —
optimistic doesn't know "is this char bound to a non-self-insert
command in the current keymap." Rare in practice; revisit if anyone
hits it. Documented at session-5 finding time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
First milestone of the M11 semantic-frontend arc
(docs/semantic-frontend-protocol.md). Wire-format scaffolding only —
no producer or consumer; mechanically identical to the M10.5 CRDT
wire declaration, and non-breaking by the same slice-membership +
per-session-filter argument.
- PROTOCOL_VERSION 2 -> 3; SUPPORTED_PROTOCOL_VERSIONS [1,2,3]. v0.1
and v1.0 binaries keep connecting unchanged (membership, not
strict equality).
- semantic_render capability bit on FrontendCapabilities,
InstanceCapabilities, NegotiatedCapabilities (#[serde(default)];
instance default false until the M11.2 projection seam).
negotiate_capabilities AND-combines it and enforces the
semantic_render => crdt_replica dependency as a CapabilityMismatch
(a semantic session is also a text replica), never a silent
degrade. PMACS_INSTANCE_SEMANTIC_RENDER env override added.
- InstanceMessage SemanticFrame family: StyleSpans, Decorations,
InlineAdornments, BlockAdornments, FoldState, ResourceOffer.
FrontendEvent::Viewport. Supporting types: ByteRange, StyleSpan,
Decoration/DecorationKind, InlineAdornment/AdornmentPlacement/
AdornmentContent, BlockAdornment, ResourceBody. All byte-anchored;
no pixels cross the contract boundary.
- Grid TUI (frontend.rs) and daemon apply_event drop the new family
silently — the "declared, not yet wired" posture CrdtOp held
between M10.5 and M10.8. Stale v1.0 version-pin tests updated to
the v1.1 truth; negotiation matrix + postcard round-trips added.
Lib + integration suites green on both the default and crdt feature
flavors; clippy -D warnings clean on both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the CI Lint regression: commit 6113c53 bumped
rust-version 1.85 -> 1.95. clippy::collapsible_if is MSRV-gated —
collapsing `if let { if let }` needs let-chains, stabilized in Rust
1.95. At MSRV 1.85 clippy suppressed these; at 1.95 it emits them.
The patterns were pre-existing; the MSRV bump surfaced 47 of them
and turned `Lint (luajit)` / `Lint (lua54)` red at HEAD (was green
through PR #7; red from PR #8 = the release-prep MSRV bump).
Resolution (operator-chosen: autofix into let-chains): applied
`cargo clippy --fix` across the luajit, lua54, and crdt lanes
(--all-targets). The fix only applied with the lint at warn level;
`-- -D warnings` turns it into an error and blocks --fix.
Verified on the pinned 1.95.0, all three lanes:
clippy --all-targets -D warnings clean (luajit / lua54 / crdt);
fmt 0 diffs; lib tests 1223/0.
Note: the prior #6 "quiescent audit, clippy clean" was inaccurate —
clippy was not actually re-run there (build/version/fmt only), so
this MSRV-gated regression went uncaught until the live attach-debug
investigation surfaced it. This commit restores genuine clippy
cleanliness at MSRV 1.95.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PMACS_ATTACH_DEBUG breadcrumbs were written via eprintln! to the
client's stderr — the same terminal the crossterm alt-screen TUI
renders on. Handshake breadcrumbs are fine (pre-TUI), but the
DebugReader emits one per protocol read, so every live-session frame
painted a debug line over the screen (buffer/protocol uncorrupted;
purely the client's own stderr stomping its own TUI).
Per the chosen "both" routing:
- Always append the full breadcrumb stream (incl. live-session
reads) to a log file: PMACS_ATTACH_DEBUG_FILE, else
<tempdir>/pmacs-attach-debug.log. Path printed once to stderr
during the handshake so the user knows where to look.
- Mirror to stderr only while it is still a normal terminal — a
TUI_TERMINAL_OWNED flag flips just before the interactive pump
(covers reconnect handshakes too, since the TUI stays up).
Safe std only (OnceLock/Mutex/File) — forbid(unsafe_code) intact.
The one new nested `if let` is written as a 1.95 let-chain so it
does not add to the MSRV-surfaced collapsible_if set (swept next).
Verified: build clean, attach lib tests 143/0, fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI was red on every recent main commit (pre-existing, not from the
V0.2/audit work): the workflow installs rolling `stable`, which on
the runners is ~1 year newer than the local toolchain that validated
the code. Under `RUSTFLAGS: -D warnings` + `clippy -- -D warnings`,
new rustc/clippy lints across pre-existing code became hard failures.
Confirmed identical on the 4 commits before v1.0-rc (e.g. the
`rope.rs:1076` unused_parens compile error is byte-identical there).
Resolution:
- `rust-toolchain.toml` pins channel 1.95.0 (the validated version).
The repo directory override makes every cargo invocation use it
regardless of what the CI action installs, eliminating the
local/CI toolchain-drift class permanently. Bump deliberately.
- Mechanical lint fixes (~17 sites, all the trivial/auto-fixable
class — no logic change): `cargo clippy --fix` + `cargo fix`
applied the machine-applicable set; hand-fixed the residuals:
daemon.rs (duplicated #[allow]), completion_framework.rs
(sort_by -> sort_by_key/Reverse), attach.rs (map().unwrap_or ->
map_or, crdt), buffer.rs (is_some+expect -> match, crdt),
m10_11_acceptance.rs (if -> match guard x2, crdt).
- `cargo fmt --all` (clippy --fix left overlay_paint.rs unformatted).
Verified clean under 1.95.0, all lanes: fmt 0 diffs; clippy
--all-targets -D warnings clean for luajit, lua54, AND crdt;
-D warnings build clean luajit+lua54; doc tests pass; lib 1223/0;
autofix-modified tests (m7_5, m8_1 incl. the Finding-2 fs_watch fix)
pass.
Scope: this clears CI red class #1 (toolchain-gap lints) only.
Independent and still triage-pending: #2 macOS F9 nix
PeerCredentials portability (Test (macos-*)), #3 M1/M4/M6 perf/fuzz
gates. Per plan, those are triaged after CI confirms #1 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the optimistic local-edit-application layer on top of the M10 CRDT
foundation: frontend-side rope replica with local edit application,
daemon-authoritative broadcast, and bidirectional cursor reconciliation.
Keystrokes feel instantaneous because the local replica answers next-render
queries before the daemon round-trip completes, while the daemon remains
the single source of truth for conflict resolution and broadcast to remote
replicas.
Architecture beats:
- BufferMirror (src/buffer_mirror.rs) holds a per-frontend rope replica
with explicit cursor-staleness tracking. Every event that may move the
active cursor or swap the active buffer marks the mirror stale; the
next CursorByte from the daemon clears it.
- CrdtOpOrigin {OptimisticReplica(FrontendId), DaemonKey} routes broadcast.
OptimisticReplica skips re-application on the originating frontend
(already applied locally); DaemonKey broadcasts to all replicas including
source -- covers Lua-driven and generated-buffer edits that bypass the
optimistic path.
- Generated buffers (*help*, *workers*, *pmacs-instance*, *errors*) funnel
apply_edit output through queue_daemon_origin_crdt_op so post-attach
CRDT upgrades don't drop their edits.
- forbid(unsafe_code) preserved throughout; loro 1.12 added as the CRDT
engine.
Audit posture: M10.10 shipped through six post-audit review rounds with
twenty-eight cumulative findings, most categorized as "incomplete
application of a prior round's mechanism." The audit doc records
grep-driven exhaustiveness as the standing countermeasure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>