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:

  1. Host lifecycle - events from the controller/runtime boundary: create, start, resume, pause, stop, destroy, request begin/end, memory pressure, visibility, and graceful drain.
  2. 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.

AreaCurrent codeWhat it meansGap
Host componentComponent : ControllerEventObserver in reaktor-coreA component receives controller events through handle(event).Events are minimal, sync-only, and not contextual.
Host eventsControllerEvent.Create/Start/Resume/Pause/Stop/Destroy/BackPressed/KeyUpThe common lifecycle vocabulary today.Android is mapped; iOS/browser/server/Worker are mostly stubs.
Android bridgeAndroidComponent 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 bridgeDarwinComponent and BaseController exist, but the controller file is empty.The seam exists.UIKit/App lifecycle mapping is not implemented.
Browser bridgeWebComponent exists as an actual ControllerEventObserver.The seam exists.No document visibility/page lifecycle mapping.
Server bridgeServerComponent exists as an actual ControllerEventObserver.The seam exists.No Spring/server startup, readiness, request, drain, or shutdown mapping.
Worker bridgeCloudflareWorker.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 lifecycleLifecycleCapability 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/detachGraph.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.
PortsPortCapability emits PortEvent.Created, Connected, Disconnected.Port wiring already has an event seam.Graph lifecycle does not expose these as first-class component callbacks.
TelemetryGraphTelemetry 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 ControllerEventTarget host event
CreateHostLifecycleEvent.Created
StartHostLifecycleEvent.Started
ResumeHostLifecycleEvent.Resumed
PauseHostLifecycleEvent.Paused
StopHostLifecycleEvent.Stopped
DestroyHostLifecycleEvent.Destroyed
BackPressedHostLifecycleEvent.BackRequested
KeyUpHostLifecycleEvent.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.

RuntimeNative sourceReaktor eventsImportant caveat
AndroidLifecycle.Event, back dispatcher, configuration callbacks, memory callbacksCreated, Started, Resumed, Paused, Stopped, Destroyed, BackRequested, ConfigurationChanged, MemoryPressureCurrent Android mapping exists for lifecycle events through BaseActivity.connect(...).
iOSUIViewController, UIApplication active/background notifications, memory warningCreated, Started, Resumed, Paused, Stopped, Destroyed, MemoryPressure, ConfigurationChangedBaseController is currently a stub; implement this as the iOS bridge.
BrowserDOMContentLoaded, pageshow, visibilitychange, pagehide, beforeunload, online/offlineCreated, Started, Resumed, Paused, Stopped, Destroyed, VisibilityChanged, ConfigurationChangedBrowsers may throttle background work; do not run critical cleanup only on unload.
Workermodule evaluation, fetch, queue, scheduled, alarm, waitUntilCreated, Started, InvocationStarted, InvocationFinished, DrainRequested when availableIsolate eviction may skip stop/destroy. Design for idempotency and durable state.
JVM serverapplication context start, readiness, request filters, graceful shutdownCreated, Started, InvocationStarted, InvocationFinished, DrainRequested, Stopped, DestroyedRequest 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 operationTarget event
Graph.attach(node) before transitionNodeAttaching
Graph.attach(node) after successful attachNodeAttached
Current node Lifecycle.RestoringGraphRestored or NodeAttaching context, depending on source
Current node Lifecycle.AttachingNodeAttached
Graph.detach(node) before closeNodeDetaching
Current node Lifecycle.SavingGraphSaving or NodeDetaching context
Current node Lifecycle.DestroyingNodeDetached / GraphDestroying
PortEvent.CreatedPortCreated
PortEvent.ConnectedPortConnected
PortEvent.DisconnectedPortDisconnected
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:

  1. Root graph receives all host events.
  2. Active child graph receives foreground/visibility/navigation events.
  3. All attached nodes receive stop/destroy/drain events.
  4. Background-capable nodes can opt into pause/stop continuation through a capability or facet.
  5. 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:

  1. Validate node identity and graph compatibility.
  2. Emit NodeAttaching.
  3. Restore node state if a snapshot or persisted state exists.
  4. Attach to graph index.
  5. Register DI, ports, telemetry, and graph event listeners.
  6. Emit NodeAttached.
  7. Auto-wire if the graph policy asks for it.
  8. Emit port connection events for any new edges.

Default detach order:

  1. Emit NodeDetaching.
  2. Stop active work owned by the node.
  3. Disconnect ports.
  4. Save state if the node owns durable state.
  5. Remove from graph index.
  6. Close node resources idempotently.
  7. 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 operationLifecycle events
Add nodeNodeAttaching, NodeAttached, optional PortCreated
Remove nodeNodeDetaching, PortDisconnected, NodeDetached
Add portPortCreated
Connect portsPortConnected
Disconnect portsPortDisconnected
Collapse into subgraphparent NodeDetaching/NodeAttached for moved nodes, child GraphCreated, GraphActivated if opened
Replace service implementationprovider 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 ControllerEvent and Component.handle(event) working.
  • Add conversion from ControllerEvent to HostLifecycleEvent.
  • Keep current LifecycleCapability states and transitions.
  • Add tests that lock current attach/detach behavior before changing the API.

Phase 1: Core Lifecycle Envelope

  • Add ReaktorLifecycleEvent, HostLifecycleEvent, ReaktorLifecycleState, ReaktorLifecycleOwner, and ReaktorComponent to reaktor-core.
  • Add a small LifecycleDispatcher that updates state, emits events, and calls component callbacks.
  • Make invalid transitions return ReaktorLifecycleResult.Rejected with a reason instead of silently disappearing.

Phase 2: Platform Bridges

  • Android: adapt existing Lifecycle.Event bridge to emit the new events.
  • iOS: implement BaseController and 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 GraphLifecycleEvent and ReaktorGraphComponent in reaktor-graph.
  • Wrap Graph.attach, Graph.detach, Graph.close, and ContainerNode.activateGraphForRoute with structured events.
  • Bridge PortEvent.Created/Connected/Disconnected into graph lifecycle events with graph, node, edge, and port context.

Phase 4: Telemetry And Engine

  • Update GraphTelemetry to consume lifecycle events rather than only collecting StateFlow and 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:

  1. Android ON_CREATE/START/RESUME/PAUSE/STOP/DESTROY map to the new host events and still support existing ControllerEvent observers.
  2. A node attach emits NodeAttaching, restore/compat transition, NodeAttached in deterministic order.
  3. A node detach emits NodeDetaching, port disconnect events, NodeDetached, and closes resources exactly once.
  4. Connecting ports emits both the existing PortEvent.Connected and the new GraphLifecycleEvent.PortConnected with the same consumer/provider identity.
  5. Worker fetch emits InvocationStarted and InvocationFinished even when no stop/destroy event is observed.
  6. Server graceful shutdown emits DrainRequested, then stop/destroy events.
  7. Browser visibility changes pause/resume foreground work without detaching the graph.
  8. A graph patch that removes a service provider disconnects ports before the provider node is closed.
  9. GraphTelemetry can record lifecycle from the new event stream without special-casing graph internals.
  10. 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 new LifecycleEventCapability, 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/Destroying be 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.