Skip to content

runtime: consolidate the post-train seams (builder tail, Role vocabulary, load-path convergence) #94

Description

@mfw78

Seam consolidation plan (converged 2026-08-06, red-teamed, maintainer decisions applied)

Maintainer decisions: one plan-epic (this issue); M2 order is seams then types (#69) then guard (#56); #76 lands standalone now; #78 is the first car (parse-reject makes the fallback consts dead before the Role car deletes them); #81 rides the builder car; #55 closed as landed in-crate; the downstream audit (item 1) starts immediately and gates the deletion bucket; item 8 (provider install deadline) is its own PR after the train and the audit.

Car order: #78 reject-blank-names -> item 2 builder-tail (+#81 clock exposure) -> item 5 BootEnv -> item 3 Role vocabulary (+#80 provider-subscription validation) -> item 4 load-path convergence -> item 6 cruft sweep -> item 7 SubscriptionPlan. Cars close #78, #81, #80 respectively; others are Part of this issue.

Ranked items (as analysed)

1. Single downstream-consumer audit (one grep pass, one keep/delete decision doc) [S] ~0 code; one decision doc

One pass over shepherd and the extension repos resolving every gated deletion the four lenses scattered: Ext = bounds and HostState.ext reads (decides RuntimeTypes::Ext + ExtState), impl SealedRuntimeTypes occurrences, engine_config quota/watch/status_poll accessors (docs already name a consuming service at engine_config.rs:486), actor.rs SupervisedStore/ActorSlot/ActorFault, build_provider_linker, IntoStaticStr on the four pub error enums (EngineConfigError/EnvVarError/DigestParseError/ParseError - Cargo.toml:28's rationale was downstream metric labels), test_utils Entry/ManifestSource From impls, ModuleId Ord/PartialOrd/AsRef, and AssembledRuntime pub-field changes (owned add_ons, PathBuf). Output is a single decision doc; expected outcomes per in-tree evidence: quota/watch KEEP, SupervisedStore KEEP, Ext axis KEEP (harness builder_with_ext/MockTypes at harness.rs:34-72,154,438 exists for downstream tests), the rest likely DELETE in a later coordinated bump. Runs in parallel with everything; only ranks first because items 2 and 8 consult it.
Depends on: none; can start immediately, in parallel with the t7/t8/docs cars
Issues: #50 #51 (StorageError shape input), #54 (published test-utils surfaces); clears epic #83's deferred-decision residue

2. Builder-tail consolidation: one open_and_launch, delete bootstrap.rs + ComponentsStage + LaunchRuntime + runtime/limits.rs [M] -120 to -180

Collapse the three copy-pasted launch bodies (builder.rs:456, :515, :649) into one private fn open_and_launch(config, extensions, add_ons, source, clocks, components_builder) that each terminal stage calls; delete bootstrap.rs outright (zero workspace consumers; its one advertised capability is already the Prebuilt slots) and runtime/limits.rs (5 doc lines, zero code), retargeting the dispatch.rs:352 doc link; LaunchRuntime dies with bootstrap (single impl at builder.rs:143-148) - its body becomes an inherent async fn launch on AssembledRuntime. Dissolve ComponentsStage: ReadyBuilder gains add_ons defaulting to empty with optional with_add_ons, TypedBuilder::with_components returns ReadyBuilder directly (every non-preset caller passes .with_add_ons(&[]) - builder.rs:982, 1016, 1108, harness.rs:156). KEEP the generic chain slot and Runtime::ChainBuilder (harness injects Prebuilt(pool) at harness.rs:152 - that slot IS the test seam). The AssembledRuntime field flips (owned AddOns killing both add_on_refs blocks, Option) land here only if the item-1 audit clears the escape-hatch API; otherwise keep the refs and flip later. nexum-launch is unaffected (uses only RuntimeBuilder, nexum-launch/src/lib.rs:36-42).
Depends on: item 1 for the AssembledRuntime pub-field flips only (core merge proceeds regardless); first car of the train
Issues: none of #21/#22/#23/#47/#50/#51/#54/#67/#11 directly; pure post-#82 residue outside epic #83's supervisor scope

3. Role as the single role vocabulary + role-keyed telemetry helpers (pre-metric-rename) [M] -80 to -110

Promote lifecycle::Role supervisor-wide (supervisor/role.rs): fallback_name() absorbs MODULE_FALLBACK_NAME/PROVIDER_FALLBACK_NAME (prepass.rs:37-41); manifest_role()='provider' and claim_role()='adapter' stay as two recorded decisions - do NOT silently reword, ledger.rs claim messages and scenario.rs:517 assert these strings. RolePass becomes { role: Role, chains: Option<&ConfiguredChains> } with the adapter None explicitly commented pointing at #80, making #80's fix a 10-line slice (flip the Option + read manifest.subscriptions in load::provider). load_providers/load_modules fold into one closure-parameterised loop. Telemetry half: report_trap(role, name, &verdict, policy, last_error), report_restart_attempt, report_restart_outcome as macro_rules/fns matching on Role internally (compile-time field keys force the two-arm match, but written once) - lifecycle.rs:402-511 shrinks ~70 lines; dispatch_to's trap arm shares ONLY the poison-transition trio (warn+counter+gauge) via report_poison - the trap logs themselves differ in level and fields (dispatch.rs:291-301 error! vs lifecycle.rs:403-414 warn!) and stay separate. Normalise the {e:#}-vs-%e accident (lifecycle.rs:497-507) to one rendering. Zero behavior change.
Depends on: t7/t8 merged (lifecycle.rs churn); MUST merge before the shepherd_->nexum_runtime_ metric rename so the rename touches Role's const fns once (lifecycle.rs:190-209)
Issues: #80 (turns its fix into a 10-line slice), epic #83 residual (T6 Role const-table follow-through); adjacent #78 (fallback naming centralised)

4. Load-path convergence: shared boot/restart pipelines, Role-parameterised store build, one Seed [M] -90 to -130

The boot-vs-restart axis becomes the parameter, not the role. Extract: fresh_run_store(shared, name, seq, spec, role) used at all four store-build sites (load.rs:227-228, 379-383; lifecycle.rs:273-279, 345-351) - per the red-team's contradiction resolution, the services argument is replaced by a Role/StoreRole parameter so store::build itself owns module-gets-shared.services vs provider-gets-empty (load.rs:380-383's cycle comment becomes one match in one place; the linker-withholding half stays in build_provider_linker, acknowledged as unreachable by any layer move). instantiate_module(shared, linker, seed, run, store) shared by load::module and LoadedModule::revive; install_provider(...) shared by load::provider and LoadedProvider::revive; callers keep the dead-permanent-vs-defer policy VISIBLE at the two call sites (load.rs:159-162, 402-404 vs lifecycle.rs:372). Health::from_init(ok) replaces the twin conditionals (load.rs:293-297, 419-423). Merge ModuleSeed/ProviderSeed into Seed { artifact, spec, event_deadline } (load.rs:48-61) with Seed::instance taking (linker, sections, store, liveness) - the red-team's corrected 4-borrow signature, collapsing the twin 7-field ProviderInstance literals (load.rs:389-397, lifecycle.rs:354-362); seed-before-install ordering is borrow-clean (install borrows &seed.artifact.component, borrow ends at return). Drop the verified-dead boot-only refuel (load.rs:253-254; dispatch.rs:216 refuels per call) - micro behavior change, one PR-body line. NO deadline wrap here (that is item 8).
Depends on: item 3 (Role type used by store::build parameter); t7/t8 merged
Issues: epic #83 residual (completes #70's convergence where T3 deliberately left the duplicated skeletons load.rs:173-299 vs 303-425 visible)

5. boot_single: BootEnv bundle + shared wire_extensions behind a register_kinds flag [S] -30 to -50

boot_single and boot share one wire_extensions with a with_provider_kinds: bool parameter - the skip is load-bearing (provider_kinds errors on a serviceless kind, admission.rs:33-39), so it becomes a named argument instead of a copied divergence (mod.rs:132-141). The derived-at-call-site argument spread (builder.rs:187-199 vs mod.rs:81-88) collapses into a small BootEnv derived once from engine_cfg - mandatory, not optional: passing engine_cfg whole still leaves 8 parameters and does NOT clear the too_many_arguments allow (red-team correction to both lenses). builder.rs:181-215 shrinks to boot(engine, linker, env, components, extensions, clocks, source_override). Zero behavior change.
Depends on: sequenced after item 2 in the train (both edit builder.rs); no semantic dependency
Issues: #23 (smaller boot_single surface eases porting its e2e tests to the harness), epic #83 residual (T3 named-phases follow-through)

6. Conservative cruft sweep (independently revertible commits) [M] ~-110

The audit-safe subset only: strip IntoStaticStr + serialize_all from the non-pub enums (LifecycleState, Role, FilterError) and fix the stale Cargo.toml:28 strum rationale; drop the inert allow attributes (mod.rs:202 cfg_attr - poisoned_count is pub so dead_code cannot fire; manifest/types.rs:70,165) and the unused DispatchOutcome Eq/PartialEq (consumed only via matches!); tighten Health/LifecycleState to pub(super)/private (referenced nowhere outside supervisor/); move the 17-item cfg(test) import ladder from supervisor/mod.rs:344-376 into tests/mod.rs and replace TestTypes/CoreTypes with preset::CoreRuntime + DefaultSupervisor = Supervisor (verified: BootScenario already defaults to CoreRuntime, scenario.rs:92 - the trait lens and cruft lens proposed this identically, one commit); delete the zero-call-site From impls in the test DSL EXCEPT Entry/ManifestSource (gated); rename the 'Sealed' docs to 'marker reserved for semver evolution' (lib.rs:20-24, runtime_types.rs:8, preset.rs:25) without deleting the traits. Follow-up commit: backoff_pause helper collapsing the five event_loop.rs retry blocks - with message + optional-field parameters, since the sites differ intentionally (module vs chain-only at 177-185, tail_block at 271-283); the shared contract is bump+sleep, not the log line. Everything downstream-visible stays out pending item 1.
Depends on: t7/t8 merged (the cfg(test) ladder and TestTypes live in supervisor/mod.rs and the test lattice); item 1 only to later harvest the gated bucket
Issues: #21 (test-lattice consolidation), #54 (test-utils surface hygiene, gated half deferred)

7. SubscriptionPlan projection + cursor subscription-identity closure [M] -30 to -60

Give the supervisor one SubscriptionPlan projection { block_chains, chain_log_subs, extension_kinds, viable: {Nothing, DeadHoldSubs, Live} } computed with ONE health filter, fixing the extension-kind asymmetry (subscriptions.rs:23/40 filter dispatchable, :95-104 do not) and moving the builder.rs:288-304 bail policy onto the plan - safe because boot-dead is permanent (no Backoff exists at launch), but observable (an extension source that today opens and idles will no longer open): own test + PR-body line. Cursor half, in the red-team's amended form: TaggedChainLog carries a compact subscription id (index into a supervisor-owned Vec of cursor keys) - NOT a bare ModuleId->key lookup, which cannot disambiguate a module's multiple resume chain-log subscriptions (chainlog_cursor_key spans chain|address|sig, cursors.rs:134-149); ChainLogCursors re-keys to BTreeMap<ModuleId, BTreeMap<Arc, u64>> so T8's Arc identity reaches the mirror, and the lazy seed read (cursors.rs:70-72) folds into boot-time mirror pre-population. ChainLogResume folds into a slimmer ChainLogSub carrying its identity from birth.
Depends on: t8 merged (ModuleId Arc identity is load-bearing for the re-key); last car of the train
Issues: epic #83 residuals (subscriptions.rs/cursors.rs completion; the T8 Arc-to-mirror gap), #80 adjacent (plan is where its validation naturally lands); groundwork for #66's viable/health gauges

8. Provider install deadline (the one downstream-contract behavior change, own PR) [S code / M contract weight] +25 to +45 (wrap + contract doc + fixture test)

Wrap both kind.install call sites in with_dispatch_deadline (dispatch.rs:355-362), closing the hung-provider-init hole: a hung install in revive parks ALL dispatch (sweep runs at dispatch.rs:26-29/85). This is NOT a verbatim mirror of the module side - the module deadline bounds only call_init (load.rs:165; instantiate_async at :229 is outside it), whereas ProviderKind::install spans instantiation + guest init + extension wiring, so the wrap imposes a NEW cancel-safety contract on every downstream ProviderKind impl (both in-tree impls are test-only; production kinds live in shepherd): a timeout drops install mid-flight and may leave a half-registered actor in the extension service. Land with (a) the contract documented on ProviderKind::install, (b) a slow-host fixture test (modules/fixtures/slow-host exists), (c) a decision on a separate install-deadline knob vs reusing event_deadline. Providers gain event_deadline via item 4's Seed merge (limits_cfg.event_deadline() at load).
Depends on: item 4 (Seed carries event_deadline), item 1 (audit confirms downstream ProviderKind impls before the contract lands); strictly last, outside the train
Issues: #22 adjacent (the deadline test is exactly what the clock seam would speed up - note the insertion point upstream), epic #83/#70 residual (symmetry with the documented module rationale, load.rs:159-162)

Killed proposals

  • S1 Roster<S: Sweepable> collection type - single-consumer generic ceremony, net +10 LOC by its own estimate; the six health-filter counts are 4-line one-liners on module-only pub API (mod.rs:174-208, 302-316); re-propose only if Sync-lag gauge pair and progressing-vs-stuck chain health #66 supplies a second real consumer.
  • Role-generic Loaded / GAT extras on ProviderKind or Sweepable - the PR-89 T6 adjudication stands; all four lenses respected it and verification produced no new structural evidence to reopen it.
  • Chain-slot demotion + Runtime::ChainBuilder deletion (inside topology step 2) - the generic C slot IS the harness test seam (Prebuilt(pool) at harness.rs:152, test_utils/mod.rs:130); an enum special-case buys one type parameter at the cost of an asymmetric harness.
  • HostState::for_run move into host/ (topology step 6) - trades a 4-arg builder for a 7+-arg one and the linker-withholding half of the policy (store.rs:195-204) stays behind regardless; superseded by the Role-parameter form inside item 4.
  • RuntimeTypes::Ext axis + ExtState deletion - 'write-only in-tree' was misleading: the harness's builder_with_ext/MockTypes/ptr_eq seam (harness.rs:34-72, :154, :438) is a published downstream test surface; expected audit outcome KEEP, and deleting it would collapse RuntimeTypes to one axis with very large churn for -60..-100 LOC.
  • Sealed-marker trait deletion - breaks any downstream impl SealedRuntimeTypes for X {}; downgraded to the doc rename riding item 6, with actual deletion deferred to a coordinated downstream bump.
  • engine_config quota/watch/status_poll deletion (~200 LOC) - the accessor docs name a consuming service (engine_config.rs:486); expected audit outcome KEEP with a doc note; the real fix for the lib.rs charter tension is extension-owned config, out of scope.
  • actor.rs SupervisedStore/ActorSlot/ActorFault deletion (~55 LOC) - it is precisely the documented helper for the downstream install-impl ownership model (extension.rs:142-143 / :162-177); expected audit outcome KEEP.
  • StateStore vocabulary move out of local_store_redb (trait item 6) - real seam smell (component/state.rs:8), but the load-bearing decision (box redb variants vs an opaque Backend variant) is exactly errors: right-size seam error types and wire From conversions #50/errors: replace stringly and anyhow error paths with typed enums #51's scope; land it inside the already-recommended errors: replace stringly and anyhow error paths with typed enums #51-scoped typed-error PR, not this train.
  • Unified trap logging across dispatch and sweep - the arms differ in level AND fields (dispatch.rs:291-301 error! with chain/event/latency vs lifecycle.rs:403-414 warn!); only the poison-transition trio is shared, and that is absorbed into item 3.
  • Pub-surface strippings now (IntoStaticStr on the four pub error enums, ModuleId Ord/PartialOrd/AsRef, build_provider_linker re-export demotion, test_utils Entry/ManifestSource From impls) - downstream-visible API; parked in item 1's gated bucket rather than deleted in this pass.
  • Do not count the two flipped gated deletions (~255 LOC quota/watch + actor.rs) as expected savings anywhere - in-tree evidence says they are consumed.

Packaging (as analysed; superseded where the decisions header differs)

One jj stacked train of items 2->5->3->4->6->7 (sequential cars, each independently revertible and CI-green, mergiraf for conflicts), based on the merged tip AFTER the in-flight t7/t8/docs cars land - items 3, 4, 6, and 7 all edit lifecycle.rs/load.rs/supervisor test lattice that T7's test conversion and T8's ModuleId rename are still churning, and item 7 requires T8's Arc identity outright; basing earlier just buys rebase noise. Two things sit OUTSIDE the train: item 1 (the downstream audit) is a no-code decision doc that starts now, in parallel with the still-merging cars; item 8 (the install deadline) is deliberately its own independent PR after the train and after the audit, because it is the only change in the entire accepted set that alters a downstream contract (cancel-safety on ProviderKind::install) - it needs its own fixture test and PR-body declaration, exactly as the boot-refuel drop (inside item 4) and the extension-kind filter change (inside item 7) each need one declared line. Hard ordering constraint: item 3 must merge before the shepherd_->nexum_runtime_ metric rename lands, so the rename edits Role's const tables once (lifecycle.rs:190-209). Honest aggregate for the accepted set: roughly net -350 to -450 LOC, with the ~255 LOC of gated deletions explicitly excluded from that figure. A second wave (gated-bucket harvest per the audit doc, plus the #51-scoped StorageError/BootRefusal PR) follows as independent PRs, not cars of this train."

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:runtimeSupervisor, builder, event loop, restart/poison lifecycle, addonsdebtRefactor/cleanup: typed replacements for stringly code, dedup, right-sizingepicUmbrella issue tracking a milestone-scale bundle of child work

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions