Reaktor Component Lifecycle
Diagnostic acceptance. Instance health measures resource ownership across repeated attach/detach, cancellation and close. Shadow client/update verification covers old/new activations, stale callbacks, migrated state and retained resources; a clean startup does not qualify a live revision transition.
Reaktor components need one lifecycle language that works across platform hosts and across the graph.
Android activities, iOS controllers, browser pages, Cloudflare Workers, and JVM servers all produce lifecycle signals. A Reaktor component should be able to react to those signals without branching on the host. At the same time, Reaktor's own graph model has lifecycle moments that do not come from a host: being attached to a graph, restored, activated, wired through ports, detached, saved, destroyed, or reconnected after a graph patch.
This document designs the lifecycle contract for both planes:
- Host lifecycle - events from the controller/runtime boundary: create, start, resume, pause, stop, destroy, request begin/end, memory pressure, visibility, and graceful drain.
- Graph lifecycle - events from Reaktor graph structure: attach, detach, activate, deactivate, ports created, ports connected, ports disconnected, snapshot, restore, and close.
The important rule is that these are related, but not the same. A node can be attached to a graph while the app is paused. A Worker can handle a request without ever receiving a reliable stop event. A server component can be started while a specific service node is not yet wired. A route can become active while the owning process has been running for hours.
- Host module
reaktor-core - Graph modules
reaktor-graph,reaktor-graph-port - Tooling consumers telemetry, Reaktor Engine, MCP, agents, tests, graph visitors, visual editor
- Status Design proposal grounded in current code
- Updated 2026-06-18
1. Current State
The code already has the start of the model, but the pieces are split.
| Area | Current code | What it means | Gap |
|---|---|---|---|
| Host component | Component : ControllerEventObserver in reaktor-core | A component receives controller events through handle(event). | Events are minimal, sync-only, and not contextual. |
| Host events | ControllerEvent.Create/Start/Resume/Pause/Stop/Destroy/BackPressed/KeyUp | The common lifecycle vocabulary today. | Android is mapped; iOS/browser/server/Worker are mostly stubs. |
| Android bridge | AndroidComponent maps Lifecycle.Event to ControllerEvent; BaseActivity.connect(...) registers adapters. | Android already proves the bridge shape. | Registration is manual; lifecycle is adapter-only, not unified with graph lifecycle. |
| iOS bridge | DarwinComponent and BaseController exist, but the controller file is empty. | The seam exists. | UIKit/App lifecycle mapping is not implemented. |
| Browser bridge | WebComponent exists as an actual ControllerEventObserver. | The seam exists. | No document visibility/page lifecycle mapping. |
| Server bridge | ServerComponent exists as an actual ControllerEventObserver. | The seam exists. | No Spring/server startup, readiness, request, drain, or shutdown mapping. |
| Worker bridge | CloudflareWorker.fetch(...) and BestBuds worker wrappers route requests. | Workers have a runtime boundary. | No Reaktor lifecycle envelope around module start, fetch, queue, scheduled, alarm, waitUntil, or isolate eviction. |
| Graph lifecycle | LifecycleCapability has Created, Restoring, Attaching, Saving, Destroying. | Graph and nodes already have a state machine. | State names are graph-specific, no event context, no port lifecycle callbacks. |
| Graph attach/detach | Graph.attach(node) transitions Restoring -> Attaching; Graph.detach(node) transitions Saving -> Destroying, then closes the node. | Attach/detach already run lifecycle and cleanup. | The operation is not modeled as a structured lifecycle event. |
| Ports | PortCapability emits PortEvent.Created, Connected, Disconnected. | Port wiring already has an event seam. | Graph lifecycle does not expose these as first-class component callbacks. |
| Telemetry | GraphTelemetry observes graph.lifecycle, navigation, and node port events. | Lifecycle and port events are already useful signals. | The event model should be explicit so telemetry is not a side observer over ad hoc state. |
The design target is not a rewrite. It is a small unification layer that makes the existing shape precise.
2. Principles
Lifecycle should follow the same Reaktor principles as the graph kernel.
2.1 Platform adapters translate, components do not branch
Shared code should never ask "am I Android, iOS, browser, Worker, or Spring?" The host adapter knows the platform and emits a normalized lifecycle event. The component receives that event.
2.2 Graph lifecycle is native, not a UI afterthought
Attach, detach, activate, port connect, and port disconnect are lifecycle events. They are not incidental implementation details. They must be visible to validation, telemetry, graph inspectors, tests, and agents.
2.3 Events carry context; states stay small
The state machine should be small enough to reason about. Rich detail belongs in event context: source, reason, graph id, node id, port id, route, request, environment, operation id, timestamp, and provenance.
2.4 close() remains the hard cleanup contract
Host stop events are not guaranteed everywhere. Workers can be evicted. Mobile
processes can die. Browser tabs can close abruptly. Reaktor should use lifecycle
events for normal cleanup, but AutoCloseable.close() and coroutine
cancellation remain the hard resource ownership boundary.
2.5 Lifecycle is observable and deterministic
Lifecycle transitions should emit stable events in stable order. That makes tests deterministic, telemetry comparable, graph snapshots explainable, and agent operations auditable.
2.6 Invalid transitions are findings, not silent mysteries
The current graph lifecycle ignores invalid transitions. The target model should either reject them with a typed result or emit a lifecycle finding. A hidden no-op is bad for Reaktor Engine and agents.
3. Two Planes, One Envelope
Use one event envelope and two event families.
interface ReaktorLifecycleOwner : AutoCloseable {
val lifecycle: StateFlow<ReaktorLifecycleState>
val lifecycleEvents: Flow<ReaktorLifecycleEvent>
suspend fun handleLifecycle(
event: ReaktorLifecycleEvent,
context: ReaktorLifecycleContext = ReaktorLifecycleContext.Empty,
): ReaktorLifecycleResult = ReaktorLifecycleResult.Accepted
}
The event family says where the event came from:
sealed interface ReaktorLifecycleEvent {
val source: LifecycleSource
val atEpochMs: Long
sealed interface Host : ReaktorLifecycleEvent
sealed interface Graph : ReaktorLifecycleEvent
}
enum class LifecycleSource {
Android,
Ios,
Browser,
Worker,
Server,
Graph,
Engine,
Test,
}
The state is deliberately coarse:
enum class ReaktorLifecycleState {
New,
Starting,
Active,
Suspended,
Stopping,
Stopped,
Destroying,
Destroyed,
Failed,
}
Graph-specific restore/save/attach detail should be events, not extra global states. That keeps the state machine usable across a Worker fetch, an Android screen, a server service, and a graph node.
4. Host Lifecycle Events
Host lifecycle events come from the controller/runtime boundary.
sealed interface HostLifecycleEvent : ReaktorLifecycleEvent.Host {
data class Created(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class Started(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class Resumed(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class Paused(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class Stopped(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class Destroyed(val reason: Reason = Reason.Normal) : HostLifecycleEvent
data class BackRequested(val handledByHost: Boolean = false) : HostLifecycleEvent
data class KeyUp(val key: String? = null) : HostLifecycleEvent
data class VisibilityChanged(val visible: Boolean) : HostLifecycleEvent
data class MemoryPressure(val level: MemoryPressureLevel) : HostLifecycleEvent
data class ConfigurationChanged(val keys: Set<String>) : HostLifecycleEvent
data class InvocationStarted(val invocation: InvocationContext) : HostLifecycleEvent
data class InvocationFinished(val invocation: InvocationContext, val outcome: InvocationOutcome) : HostLifecycleEvent
data class DrainRequested(val deadlineEpochMs: Long?) : HostLifecycleEvent
}
The old ControllerEvent maps cleanly into this:
Current ControllerEvent | Target host event |
|---|---|
Create | HostLifecycleEvent.Created |
Start | HostLifecycleEvent.Started |
Resume | HostLifecycleEvent.Resumed |
Pause | HostLifecycleEvent.Paused |
Stop | HostLifecycleEvent.Stopped |
Destroy | HostLifecycleEvent.Destroyed |
BackPressed | HostLifecycleEvent.BackRequested |
KeyUp | HostLifecycleEvent.KeyUp |
Keep ControllerEvent as a compatibility adapter while introducing the new
event model.
5. Host Mapping By Runtime
Different hosts provide different guarantees. Reaktor should normalize what is available and not pretend every target has Android's lifecycle.
| Runtime | Native source | Reaktor events | Important caveat |
|---|---|---|---|
| Android | Lifecycle.Event, back dispatcher, configuration callbacks, memory callbacks | Created, Started, Resumed, Paused, Stopped, Destroyed, BackRequested, ConfigurationChanged, MemoryPressure | Current Android mapping exists for lifecycle events through BaseActivity.connect(...). |
| iOS | UIViewController, UIApplication active/background notifications, memory warning | Created, Started, Resumed, Paused, Stopped, Destroyed, MemoryPressure, ConfigurationChanged | BaseController is currently a stub; implement this as the iOS bridge. |
| Browser | DOMContentLoaded, pageshow, visibilitychange, pagehide, beforeunload, online/offline | Created, Started, Resumed, Paused, Stopped, Destroyed, VisibilityChanged, ConfigurationChanged | Browsers may throttle background work; do not run critical cleanup only on unload. |
| Worker | module evaluation, fetch, queue, scheduled, alarm, waitUntil | Created, Started, InvocationStarted, InvocationFinished, DrainRequested when available | Isolate eviction may skip stop/destroy. Design for idempotency and durable state. |
| JVM server | application context start, readiness, request filters, graceful shutdown | Created, Started, InvocationStarted, InvocationFinished, DrainRequested, Stopped, Destroyed | Request lifecycle is per invocation; service lifecycle is per server process. |
Worker and server runtimes need invocation events because a long-lived process lifecycle alone is not enough. A Worker component can be "started" once and handle thousands of invocations. A server component can be active while one request fails and another succeeds.
6. Component API
The public component surface should be simple. Most component authors should override named callbacks, while framework code works with the event stream.
interface ReaktorComponent : ReaktorLifecycleOwner {
suspend fun onCreate(context: ReaktorLifecycleContext) {}
suspend fun onStart(context: ReaktorLifecycleContext) {}
suspend fun onResume(context: ReaktorLifecycleContext) {}
suspend fun onPause(context: ReaktorLifecycleContext) {}
suspend fun onStop(context: ReaktorLifecycleContext) {}
suspend fun onDestroy(context: ReaktorLifecycleContext) {}
suspend fun onInvocationStarted(context: InvocationContext) {}
suspend fun onInvocationFinished(context: InvocationContext, outcome: InvocationOutcome) {}
suspend fun onMemoryPressure(level: MemoryPressureLevel) {}
override suspend fun handleLifecycle(
event: ReaktorLifecycleEvent,
context: ReaktorLifecycleContext,
): ReaktorLifecycleResult {
when (event) {
is HostLifecycleEvent.Created -> onCreate(context)
is HostLifecycleEvent.Started -> onStart(context)
is HostLifecycleEvent.Resumed -> onResume(context)
is HostLifecycleEvent.Paused -> onPause(context)
is HostLifecycleEvent.Stopped -> onStop(context)
is HostLifecycleEvent.Destroyed -> onDestroy(context)
is HostLifecycleEvent.InvocationStarted -> onInvocationStarted(event.invocation)
is HostLifecycleEvent.InvocationFinished -> onInvocationFinished(event.invocation, event.outcome)
is HostLifecycleEvent.MemoryPressure -> onMemoryPressure(event.level)
else -> Unit
}
return ReaktorLifecycleResult.Accepted
}
}
The default behavior does nothing. That preserves the current Component
ergonomics: implement only what you need.
7. Graph Lifecycle Events
Graph lifecycle events come from Reaktor structure.
Moved. The GraphLifecycleEvent vocabulary now lives in
Reaktor Graph Protocol §3, because the same event
set is what crosses a trunk when a workbench, an MCP server or a peer runtime subscribes
to a graph. Defining it in two places would let the in-process contract and the wire
contract drift apart.
The mapping onto the code that exists today stays here — it is a lifecycle-implementation concern, not a protocol one.
These events map to code that already exists:
| Current operation | Target event |
|---|---|
Graph.attach(node) before transition | NodeAttaching |
Graph.attach(node) after successful attach | NodeAttached |
Current node Lifecycle.Restoring | GraphRestored or NodeAttaching context, depending on source |
Current node Lifecycle.Attaching | NodeAttached |
Graph.detach(node) before close | NodeDetaching |
Current node Lifecycle.Saving | GraphSaving or NodeDetaching context |
Current node Lifecycle.Destroying | NodeDetached / GraphDestroying |
PortEvent.Created | PortCreated |
PortEvent.Connected | PortConnected |
PortEvent.Disconnected | PortDisconnected |
ContainerNode.activateGraphForRoute(...) | GraphActivated, GraphDeactivated |
The current LifecycleCapability can stay as the compatibility state machine
while the new graph event stream is added around it.
8. Graph Lifecycle Subinterface
Components that participate in the graph should implement a graph-specific subinterface. This keeps host lifecycle and graph lifecycle separate.
interface ReaktorGraphComponent : ReaktorComponent {
val graphLifecycle: Flow<GraphLifecycleEvent>
suspend fun onAttach(context: GraphAttachContext) {}
suspend fun onAttached(context: GraphAttachContext) {}
suspend fun onDetach(context: GraphDetachContext) {}
suspend fun onDetached(context: GraphDetachContext) {}
suspend fun onGraphActivated(context: GraphActivationContext) {}
suspend fun onGraphDeactivated(context: GraphActivationContext) {}
suspend fun onPortCreated(context: PortLifecycleContext) {}
suspend fun onPortConnected(context: PortConnectionContext) {}
suspend fun onPortDisconnected(context: PortConnectionContext) {}
suspend fun onBeforeSnapshot(context: GraphSnapshotContext) {}
suspend fun onAfterRestore(context: GraphSnapshotContext) {}
}
That interface should be implemented by Graph, Node, and graph-aware
adapters. Plain reaktor-core components do not need it.
Why this is a subinterface
A storage adapter, permission adapter, or analytics sink may need host lifecycle
events but not graph lifecycle events. A Node, ServiceNode, RouteNode, or
ContainerNode needs graph lifecycle events. A Worker wrapper may need both.
Splitting the interfaces keeps the core layer graph-neutral, matching the
existing reaktor-core rule: code that depends on graph semantics belongs in
reaktor-graph.
9. Propagation Rules
Lifecycle propagation must be predictable.
Host to root graph
The host bridge dispatches host lifecycle events to the app/runtime root. The root decides which graph scopes receive the event.
Default policy:
- Root graph receives all host events.
- Active child graph receives foreground/visibility/navigation events.
- All attached nodes receive stop/destroy/drain events.
- Background-capable nodes can opt into pause/stop continuation through a capability or facet.
- Invocation events go only to the graph/service/worker scope handling that invocation, plus telemetry/audit observers.
Graph attach/detach
Attach/detach are graph operations. They should not depend on host state.
Default attach order:
- Validate node identity and graph compatibility.
- Emit
NodeAttaching. - Restore node state if a snapshot or persisted state exists.
- Attach to graph index.
- Register DI, ports, telemetry, and graph event listeners.
- Emit
NodeAttached. - Auto-wire if the graph policy asks for it.
- Emit port connection events for any new edges.
Default detach order:
- Emit
NodeDetaching. - Stop active work owned by the node.
- Disconnect ports.
- Save state if the node owns durable state.
- Remove from graph index.
- Close node resources idempotently.
- Emit
NodeDetached.
This is close to what Graph.attach and Graph.detach already do, but with
structured events and explicit ordering.
Port connect/disconnect
PortCapability already emits port events. The graph lifecycle layer should
listen once and re-emit them with graph context:
PortEvent.Connected
-> GraphLifecycleEvent.PortConnected
-> ReaktorGraphComponent.onPortConnected
-> GraphTelemetry span event
-> Reaktor Engine inspector update
No component should need to poll port state to learn that wiring changed.
10. Lifecycle And Graph Patches
Lifecycle is part of graph mutation.
When a human or agent applies a GraphPatch, lifecycle events should describe
what happened:
| Patch operation | Lifecycle events |
|---|---|
| Add node | NodeAttaching, NodeAttached, optional PortCreated |
| Remove node | NodeDetaching, PortDisconnected, NodeDetached |
| Add port | PortCreated |
| Connect ports | PortConnected |
| Disconnect ports | PortDisconnected |
| Collapse into subgraph | parent NodeDetaching/NodeAttached for moved nodes, child GraphCreated, GraphActivated if opened |
| Replace service implementation | provider PortDisconnected, provider NodeDetached, provider NodeAttached, provider PortConnected |
This gives the visual editor, tests, telemetry, and agents the same mutation story. No one has to infer lifecycle from a before/after snapshot diff.
11. Runtime Examples
Android screen node
An Android Activity emits host lifecycle through the adapter bridge. A route node receives graph activation when the route becomes visible.
Activity.ON_CREATE
-> HostLifecycleEvent.Created
-> root graph onCreate
Graph.attach(ChatRouteNode)
-> NodeAttaching
-> NodeAttached
Navigate to /chat/{id}
-> NodeActivated(reason = RouteVisible)
-> host may already be Active
Activity.ON_STOP
-> HostLifecycleEvent.Stopped
-> active graph flushes analytics and pauses foreground-only work
iOS controller
iOS should map controller/app notifications into the same host events. A graph node does not learn whether the event came from UIKit or AndroidX.
viewDidLoad -> Created
viewWillAppear -> Started
applicationDidBecomeActive -> Resumed
applicationWillResignActive -> Paused
applicationDidEnterBackground -> Stopped
deinit / view controller release -> Destroyed when Reaktor owns the controller
Browser app
Browser lifecycle should separate page visibility from graph activation.
DOMContentLoaded -> Created
pageshow -> Started
visibilitychange(hidden=false) -> Resumed + VisibilityChanged(true)
visibilitychange(hidden=true) -> Paused + VisibilityChanged(false)
pagehide -> Stopped
beforeunload -> best-effort Destroyed
Worker service
A Worker should not rely on stop/destroy. It should model invocations and use durable state for work that must survive isolate eviction.
module evaluated -> Created + Started
fetch(request) -> InvocationStarted(kind = Fetch)
service handler runs
ctx.waitUntil(flush telemetry)
response produced -> InvocationFinished(success)
isolate evicted -> no guaranteed Stopped/Destroyed
JVM server
A JVM server has both process lifecycle and request lifecycle.
ApplicationStarting -> Created
ApplicationReady -> Started + Resumed
HTTP request filter entry -> InvocationStarted(kind = Http)
handler completes -> InvocationFinished(success/failure)
SIGTERM / readiness false -> DrainRequested
context closed -> Stopped + Destroyed
12. Implementation Plan
Phase 0: Keep Compatibility
- Keep
ControllerEventandComponent.handle(event)working. - Add conversion from
ControllerEventtoHostLifecycleEvent. - Keep current
LifecycleCapabilitystates and transitions. - Add tests that lock current attach/detach behavior before changing the API.
Phase 1: Core Lifecycle Envelope
- Add
ReaktorLifecycleEvent,HostLifecycleEvent,ReaktorLifecycleState,ReaktorLifecycleOwner, andReaktorComponenttoreaktor-core. - Add a small
LifecycleDispatcherthat updates state, emits events, and calls component callbacks. - Make invalid transitions return
ReaktorLifecycleResult.Rejectedwith a reason instead of silently disappearing.
Phase 2: Platform Bridges
- Android: adapt existing
Lifecycle.Eventbridge to emit the new events. - iOS: implement
BaseControllerand UIKit/application notification mapping. - Browser: add document/page visibility mapping.
- Worker: add lifecycle wrapper around
CloudflareWorker.fetch, queue, scheduled, and alarm entrypoints. - JVM server: add Spring application and request lifecycle adapters.
Phase 3: Graph Lifecycle Events
- Add
GraphLifecycleEventandReaktorGraphComponentinreaktor-graph. - Wrap
Graph.attach,Graph.detach,Graph.close, andContainerNode.activateGraphForRoutewith structured events. - Bridge
PortEvent.Created/Connected/Disconnectedinto graph lifecycle events with graph, node, edge, and port context.
Phase 4: Telemetry And Engine
- Update
GraphTelemetryto consume lifecycle events rather than only collectingStateFlowand port listeners directly. - Add graph lifecycle rows to Reaktor Engine inspectors.
- Surface lifecycle findings: invalid transition, missing stop, unclosed resource, disconnected required port, late event after destroy.
Phase 5: Commands, Tests, And Agents
- Make graph patch application emit lifecycle events in deterministic order.
- Add snapshot tests for attach/detach/connect/disconnect event order.
- Add MCP/agent read APIs for current lifecycle state and recent lifecycle events.
- Add command gates for lifecycle-sensitive operations such as stop, drain, restart, detach, or replace service provider.
13. Acceptance Tests
The first implementation should prove:
- Android
ON_CREATE/START/RESUME/PAUSE/STOP/DESTROYmap to the new host events and still support existingControllerEventobservers. - A node attach emits
NodeAttaching, restore/compat transition,NodeAttachedin deterministic order. - A node detach emits
NodeDetaching, port disconnect events,NodeDetached, and closes resources exactly once. - Connecting ports emits both the existing
PortEvent.Connectedand the newGraphLifecycleEvent.PortConnectedwith the same consumer/provider identity. - Worker fetch emits
InvocationStartedandInvocationFinishedeven when no stop/destroy event is observed. - Server graceful shutdown emits
DrainRequested, then stop/destroy events. - Browser visibility changes pause/resume foreground work without detaching the graph.
- A graph patch that removes a service provider disconnects ports before the provider node is closed.
GraphTelemetrycan record lifecycle from the new event stream without special-casing graph internals.- Invalid transitions produce a typed rejection or finding visible to tests and Reaktor Engine.
14. Open Decisions
- Should lifecycle callbacks be
suspend, or should the dispatcher require callbacks to launch work explicitly? - Should the event stream live on
LifecycleCapability, a newLifecycleEventCapability, or both? - Should graph attach validate all required ports before
NodeAttached, or attach first and emit a lifecycle finding for missing required ports? - Which events should be durable in a local event log versus transient telemetry?
- What is the stable source identity for lifecycle events generated from imported non-Reaktor code?
- Should
Lifecycle.Created/Restoring/Attaching/Saving/Destroyingbe renamed, or kept as graph-internal compatibility states?
15. Summary
Reaktor needs lifecycle as a first-class, graph-visible contract.
The design is intentionally small:
platform host event
-> HostLifecycleEvent
-> ReaktorComponent callback + event stream
graph mutation or graph state change
-> GraphLifecycleEvent
-> ReaktorGraphComponent callback + event stream
both
-> telemetry, Engine inspector, tests, visitors, MCP, agents
That gives Android, iOS, browser, Worker, and server components one lifecycle language without hiding their different guarantees. It also gives Reaktor's graph-native moments - attach, detach, activate, port connect, port disconnect, snapshot, restore, and close - the same visibility as platform lifecycle.
The result matches Reaktor's principles: platform differences stay in adapters, graph facts stay graph-native, events are explicit, code remains truth, resources close idempotently, and the control plane can see what is happening instead of guessing from side effects.