# Runtime

The `PluginRuntime` is the lifecycle engine. It owns the global scope, spawns sessions, holds the current `RuntimeSettings` snapshot, exposes a change stream, and applies updates across the runtime and its active sessions in one call.

This page covers `PluginRuntime`. For session-specific concerns (isolation, cross-scope routing, per-session contexts), see [Sessions](https://plugin-kit.saad-ardati.dev/concepts/sessions/).

## Constructing and initializing

```dart
extractRegion(pluginLifecycleDart, 'runtime-construct-and-session')
```

`PluginRuntime` is generic over the global and session context types: `PluginRuntime<G extends GlobalPluginContext, S extends SessionPluginContext>`. Bare `PluginRuntime` infers the defaults; supply your own subtypes only when you need a [custom context](https://plugin-kit.saad-ardati.dev/concepts/custom-context/).

## What `init` does

"Set up the world, then attach everything that was supposed to be on."

When you call `runtime.init(...)`, this happens in order:

1. The global registry and global bus are created.
2. Global service overrides from settings are parsed into `LocalPluginOverride`s.
3. The runtime walks the plugin list and decides which global plugins are enabled.
4. Each enabled global plugin runs `register` against a `ScopedServiceRegistry` that already knows its `pluginId`.
5. The global context is built (default, or via your `globalContextFactory`).
6. Wildcard winner-scoped overrides get materialized onto the winners.
7. Each enabled global plugin runs `attach(context)`. The runtime currently visits plugins in the order they were added, but this ordering is not part of the contract; plugin code must not rely on a particular peer being attached first. Resolve services at point of use, and use events or `resolveAfter` to coordinate between plugins.

After that, the runtime is ready to spawn sessions. You can open one per user action, keep one long-lived session, or mix the two.

Plugins not listed in `RuntimeSettings.plugins` default on, unless they carry `FeatureFlag.experimental`, which defaults off. Listing only some plugin ids does not narrow the enabled set by itself. To narrow it, add explicit `enabled: false` entries for plugins you want off, then pass those settings to `init` (or, later, to `updateSettings`).

## Settings reconciliation

Users toggle features. Config values change. Priorities shift. You want the runtime to converge on the new state without writing hand-rolled teardown code each time. Reconciliation is the runtime's job.

### Full reconciliation

```dart
extractRegion(pluginLifecycleDart, 'runtime-update-settings')
```

That reconciles the global scope, reconciles every active session, and publishes the new snapshot on `settingsStream`. If the `analytics` plugin was attached, it gets detached. If something was waiting on a capability that `analytics` provided, the ripples show up during this call.

### Snapshot-only update

```dart
extractRegion(pluginLifecycleDart, 'runtime-snapshot-then-reconcile')
```

`updateSettingsSnapshot` replaces the stored settings and emits on the stream. It does not run reconciliation, and it does not inject new config into already-resolved services. Think of it as "tell listeners, leave the runtime alone."

| Method | What it does |
|---|---|
| `updateSettings(...)` | Full reconciliation across runtime and sessions, plus stream emission. |
| `updateSettingsSnapshot(...)` | Stream emission only; runtime is untouched. |

`updateSettingsSnapshot` is for bookkeeping updates, preview states, optimistic UI, anything where you are confident the runtime does not need to converge. If the distinction feels unclear at a call site, reach for `updateSettings`. Reconciling more than you needed to is cheaper than skipping reconciliation you needed.

## Enablement precedence

When deciding which plugins are enabled for a given settings snapshot, the runtime applies these rules in order:

1. `FeatureFlag.locked` wins immediately. Nothing can disable a locked plugin.
2. Explicit `RuntimeSettings.plugins[pluginId].enabled` is respected next.
3. Otherwise the experimental heuristic: stable plugins default on, experimental plugins default off.

Dependency validation runs after that base pass. If a plugin depends on something disabled, it gets disabled too. The exception is locked plugins: they stay enabled and the runtime logs a configuration error for the missing dependency.

## Reconciliation is symmetric across scopes

Global and session scopes run the same lifecycle on a toggle.

- **Disable** → `plugin.detach(context)` runs, then the plugin's services are unregistered.
- **Enable** → `plugin.register(scopedRegistry)` runs, then `plugin.attach(context)`.
- **Survivor** → every still-enabled plugin receives `onPluginSettingsChanged(oldContext, newContext)` so it can re-read its config and adjust without being re-attached.

This page covers what the runtime does during reconciliation. The lifecycle hooks themselves are documented in [Plugins](https://plugin-kit.saad-ardati.dev/concepts/plugins/) and [Plugin Services](https://plugin-kit.saad-ardati.dev/concepts/plugin-services/). `attach` and `detach` are pure user hooks; the framework runs orchestration (stateful service attach, subscription cleanup) around them, so no `super` call is needed.

## Related reading

[Sessions](https://plugin-kit.saad-ardati.dev/concepts/sessions/)
  [Plugins](https://plugin-kit.saad-ardati.dev/concepts/plugins/)
  [Service Registry](https://plugin-kit.saad-ardati.dev/concepts/service-registry/)
  [Settings & Overrides](https://plugin-kit.saad-ardati.dev/guides/settings/)