Boot composition — generic-primitives-first + per-subsystem `install`
editor_boot.rs is a ~1500-line function that accumulates per-subsystem wiring. Every async subsystem (LSP, multibuffer, emacs-keys, diff, terminal, the Claude Code IDE peer) hand-wires the same six things: mode registration, command registration, service registration, an async_landed wake, a per-tick drain, and a deferred install for handles that don't exist yet when the subsystem's own handle is created. This is a maintainability + correctness-by-discipline smell. It is not a runtime-UX cost (boot runs once; the per-tick drain aggregate is sub-µs, see benchmarks.md I1.1) — but it makes good UX a less reliable outcome and the host harder to evolve. This fragment specifies the restructure. Sequencing: docs/dev/operations/slice-plans/archive/boot-composition.md.
1. The two concrete pains
Boot-ordering hazard. Several generic handles are created late because they depend on renderer-created state —
render_state_arc(the publishedArc<ArcSwap<RenderState>>), the renderer'sBufferStore, and from them theDiagnosticsQueryHandle+ theTickCallbackRegistryservice. But a subsystem's own handle is needed early (its ex-commands capture it, and theCommandRegistryis frozen early). The collision forces fragile two-phase wiring — e.g. the Claude Code peer spawns its server early but must callhandle.install_read_services(buffer_store, diagnostics)late, once those generic handles finally exist. The ordering is implicit and easy to break.Wake-robustness is by-discipline. Off-keystroke async work only reaches the screen if something calls
async_landed.notify_one()so the actor runsrun_tick_pending(the §12 / B-series event-driven wake). Today each subsystem must remember to wire that —lsp_diagnostics.set_wake(...), theMultibufferExcerptsReadyforwarder task, the L1c LSP-refresh forwarders. Theeditor_boot.rsL1c comment documents the bug class verbatim: forget it and a refresh "only reaches the screen on the next keypress." Paramount goal #4 wants async-correctness architectural, not by discipline — this is by discipline.
2. Move 1 — generic primitives first, then subsystems
Restructure boot into two ordered phases:
Phase A — primitives. Create every host-exposed generic primitive up front: the event bus, the mode / command / service registries, the tick-callback registry, the buffer-store handle, the diagnostics-query handle, the published render-state cell, the
async_landedwake handle, the runtime handle. Handles that currently depend on renderer state are created as forwardable cells (Arc<ArcSwap<…>>/Arc::default()) in Phase A and seated when the renderer wiring runs — so the handle exists early even though the content goes live later. (This is the trickiest part; see §5.)Phase B — subsystem install. Each subsystem self-installs through one crate-owned entry point,
Subsystem::install(boot: &mut BootContext), which does all of its wiring against the Phase-A primitives. Order-independent: every primitive already exists, so there is no "late handle" and no deferred install.
editor_boot.rs converges to three parts: (1) build the Phase-A primitives, (2) register the host-native builtins (the vim grammar, ex-commands, the foundation / language / oil / file-tree / snippet / tutor / buffer-kind modes, host actions — via the *_mut registry seams), then (3) the Phase-B subsystem.install(boot) list. The BC.final review (2026-06-25) corrected an earlier "two lists, *_mut removed" framing: the builtins are host-native, not subsystems, so they register inline by design and the *_mut seam is permanent. The acid test (below) governs part 3.
3. Move 2 — a BootContext that hands over primitives and bakes in robustness
BootContext is the host's generic-primitive surface made explicit. Beyond the registries, it exposes the operations that are easy to get wrong as primitives that cannot be wired without their safety property:
inbound::<T>(handler) -> Sender<T>— the bundled inbound primitive. Creates a channel whosesendwakesasync_landed(the wake is inside the sender — structurally impossible to forget) and whose items are drained per-tick via the tick-callback registry, each run throughhandler(validate → map to existingEffect→ resolve oneshot). This generalizes LSP's hand-rolled inbound buses and the Claude Code peer'sClaudeCodeInboundinto one primitive.inbound_raw::<T>() -> (Sender<T>, Receiver<T>)— the host-drained variant (BC.8d/e). Same wake-baked sender, but returns the raw receiver instead of a handler-drain closure: for server-initiated work whose apply is irreducibly&mut Editorand carries types that can't cross theEffectboundary (LSPworkspace/applyEdit; theshowMessageRequestpicker — a deferred user choice), the host seats the receiver on theEditorand drains it itself, whilesendstill wakes off-keystroke. Keeps the irreducible apply host-side without an internal-pumpEffect.wake_on_event::<E>()— subscribe a typed event and wakeasync_landed; generalizes theMultibufferExcerptsReady/ L1c forwarder tasks.tick_callback(closure)— register a per-tick drain (the I1 registry). Not the default choice. A bare tick callback has no wake of its own, so its results sit until something else firesasync_landed— in practice the user's next keystroke. Reach for it only when another producer already guarantees the wake; otherwise useinbound, whose sender carries it.commands_mut/modes_mut/services_mut/register_service/service::<T>()— the registry seams (the registration fns take&mut CommandRegistry/&mut ModeRegistry, so*_mutis the natural seam;service::<T>()is the generic lookup a subsystem uses to reach a handle another layer owns — e.g. the LSPDiagnosticsQueryHandle— without the surface naming that type).
The acid test becomes structural: a new subsystem touches editor_boot.rs in exactly one place — its entry in the Phase-B list — and zero host internals.
Effects an inbound handler returns are late, and must say so
The handler maps an item to existing Effects, but those effects apply at a moment the producer did not choose — after two git calls, an LSP round-trip, a file walk. Any effect whose meaning depends on which buffer is in front of the user therefore needs to name its buffer, because focus may have moved since the work started. Effect::ApplyEdit carries target for this reason; Effect::CursorMoveIn { target, position } (MG.18d) is the cursor peer, applied only while target is focused and dropped otherwise. Returning a bare Effect::CursorMove from an inbound handler is the bug: it lands the caret at a position computed in one buffer, in whichever buffer the user moved to.
The surface is a trait (SubsystemBoot), not the concrete bundle
install(boot) must be crate-owned (mode-ownership), so a subsystem crate has to name the type it receives. The concrete BootContext lives in lattice-host, which depends on every subsystem crate — naming it from a subsystem would be a dependency cycle. So the install surface is a trait, SubsystemBoot, in lattice-mode (below every subsystem crate; zero new deps — its bound types are all already lattice-mode deps). BootContext implements it; installs take &mut impl SubsystemBoot (static dispatch — the generic methods make the trait non-object-safe, which is fine). The trait exposes only the generic surface; host-only lifecycle (registry freezing, the into_registrations tick-token hand-off, the published-render-state cell) stays inherent on BootContext and never leaks to subsystems. The one LSP-specific handle a read-tool subsystem needs — DiagnosticsQueryHandle — is reached via the generic service::<T>() (the host registers it as a Phase-A service), so the trait names no lattice-lsp type. Rejected alternative: move BootContext down to lattice-mode — viable (post-diagnostics-drop it holds no host-only types), but the trait keeps the concrete bundle in the host where its host-specific extras live and lets subsystems depend on the capability, not the host (decided with Dhruva, 2026-06-23, weighing low churn + clean API for a mode count in the hundreds).
4. Paramount-goal alignment
UX (higher court): no runtime-UX change (boot is startup-only; the inbound wake + drain are the same mechanism, just bundled). The win is that good UX becomes the reliable outcome — the off-keystroke-wake bug class is designed out, not guarded by review. #1 perf: unchanged. Same tasks/subscriptions, fewer ad-hoc; the per-tick cost is identical. #2 extensibility: a new subsystem (incl. the future WASM plugin host) installs through the uniform
BootContext— the cleanest extension seam. #4 async: the wake is a property of the primitive, so off-keystroke delivery is guaranteed by construction — meeting the "architectural, not by discipline" bar #4 demands. Heuristic #1 (long-term fit, on merit): this is the genuinely-better design — it removes a real bug class and a real ordering hazard, and relocates wiring into the owning crate (mode-ownership). Not novelty; the merit is named.
5. Risks / hard parts
- Cross-cutting. Touches LSP, multibuffer, emacs-keys, diff, terminal, claude-code. Each migrates one slice at a time, behaviour-pinned first (§ slice plan BC.2).
- Phase-A hoisting — re-assessed (the "forwardable cell" worry was wrong). An earlier draft of this fragment expected
render_state_arcand the renderer'sBufferStoreto be "created during renderer wiring", requiring forwardable cells seated later. Reading the code (2026-06-23, BC.3 planning) corrected that:editor_boot.rsis renderer-agnostic;render_state_arcisArc::new(ArcSwap::from_pointee(RenderState::default()))— a default-init cell populated at runtime bypublish_render_state— and theBufferRegistryis created + immediately seeded with the initial document and shared byArcclone. Neither depends on anything created before it, so Phase-A hoisting is a mechanicallet-binding reorder, not a seat-content-later refactor. The actual hazard is narrower: preserve the "same Arc identity" invariants —render_state_arc,async_landed, and the overlay/cells worker cells are shared by Arc identity across worker spawns, Editor fields, and service registrations; reconstruct one instead of moving it and the worker's writes stop being observable throughRenderState::load_full. The genuinely structural facts are thatServiceRegistryis created and populated late (it reads mid-boot values) andCommandRegistryfreezes mid-boot before itsArcis consumed — both handled by sequencing, not forwardable cells. - Boot is load-bearing. A behaviour change here ripples everywhere; the regression-pin tests (BC.2) are mandatory before each migration, not after.
6. Rejected alternatives
- Leave boot as-is. Works, but the wake bug class stays by-discipline, the ordering hazard stays, and each subsystem keeps scattering ~6 calls. "It works / it's well-tested" is explicitly not sufficient (heuristic #1).
- A wiring macro per subsystem. Hides the scatter but fixes neither ordering nor ownership; cosmetic.
- A full runtime DI container. Over-engineered.
BootContextis a typed bundle handed to compile-time-known subsystems, not a runtime registry of everything.
7. Relationship to the IDE-protocol work
The Claude Code peer (I-series) is the immediate motivator and the first migration target (newest + smallest surface). I3 (write tools) lands on the current two-phase wiring but bakes the async_landed wake into ClaudeCodeInboundBus::send already — so the I-series rebases onto the BootContext inbound::<T> primitive cleanly at BC.3 with no behaviour change.