Native Component Lifecycle Kernel
Status: implemented foundation, 2026-08-22.
Decision
Zuno will adapt the lifecycle guarantees behind Cordis as a native Rust component kernel. It will not embed Cordis, load its JavaScript ABI, or copy its package layout.
Zuno is unreleased. The existing Component::mount and MountContext::on_close API will be replaced directly. There will be no compatibility facade and every internal caller will move in the same change.
The kernel owns one enforceable postcondition:
After an unload reports
Stopped, the component cannot receive a new call, no component-owned task or process remains live, and every framework-owned registration, listener, connection, route, and service has been removed.
A timeout, lost process response, or failed disposer never reports Stopped. It reports Uncertain, retains diagnostics, and is never mechanically replayed. The kernel does not claim that unloading can reverse an already-completed external mutation such as a remote API write. Such operations remain durable facts and require an explicit compensating action.
Problems being removed
- The current cleanup closure cannot fail, time out, or describe an uncertain result.
- Replacement starts a candidate while the previous component still owns exclusive resources.
- Runtime state and services are removed before cleanup proves quiescence.
- A service dependency is discovered only by an immediate
require<T>()lookup. Provider replacement does not reactivate consumers. TurnHostassembles and owns most product services outside components.- TUI exit and host replacement drop or abort owners instead of awaiting their authoritative shutdown path.
- Declarative extension state commits before the active host composition has successfully changed.
- Clients cannot inspect component lifecycle state or cleanup failures through one frontend-neutral projection.
Lifecycle model
Component preparation
Component::prepare is a side-effect-free planning phase. It may:
- provide typed services;
- require typed services staged by an earlier component or inherited from a parent scope;
- provide or require validated runtime-named capability descriptors while native executable objects remain on the typed service plane;
- register deferred effects;
- validate configuration.
It must not spawn, bind, subscribe, write, or publish directly. A component that needs one of those operations registers a deferred effect with EffectScope.
Preparation failure drops unstarted effect factories and exposes no candidate service.
Effects
An effect has a stable component-local identifier and two phases:
startacquires the resource and returns its exact asynchronous disposer;stoprequests shutdown and waits until the resource is quiescent.
start must either return an owned disposer or leave no live resource. If an operation creates a resource and later fails, that operation cleans the resource before returning its error.
Effects start in registration order and stop in reverse order. A start failure stops every already-started candidate effect before returning.
Composition transition
Every mutation builds a complete candidate composition:
- validate component and bundle identifiers;
- prepare all candidate components against a staging service view;
- mark the runtime
Stoppingand make old local services and named capability routes unavailable; - stop the previous composition completely;
- if any old effect is not known stopped, mark the runtime
Uncertainand do not start the candidate; - start candidate effects in component order;
- publish all candidate services and named capability descriptors atomically, then mark every component
Active; - if candidate start fails, stop the partial candidate and restore the previous definition through a fresh prepare/start cycle;
- if restoration fails, expose no local services and mark the runtime
FailedorUncertain.
This deliberately prioritizes exclusive-resource safety over zero-downtime replacement. A future provider-specific handoff protocol may opt into overlap, but overlap is not the default lifecycle contract.
Adding, replacing, removing, or changing a profile recomposes consumers that resolved services from the changed scope. The first implementation may recompose the complete local scope; the runtime still records exact requires/provides ownership so a later optimization can calculate a minimal dependency closure without changing behavior.
State and diagnostics
Runtime and component projections use these states:
PreparingActiveStoppingStoppedFailedUncertainClosedfor the runtime scope
Lifecycle diagnostics record:
- runtime and component identifiers;
- effect identifier;
- phase (
prepare,start,stop,restore); - typed failure kind (
rejected,failed,timed_out,uncertain); - a scrubbed message.
Cleanup has a validated positive timeout. Timeout cancellation drops the wait future but never implies the external resource stopped.
Ownership rules
The following registrations must be acquired through an EffectScope adapter:
| Resource | Stop contract |
|---|---|
| Typed service | Remove before stopping dependent activity |
| Named capability route | Withdraw its exact owner/generation before provider cleanup |
| Tool/provider/hook/route registration | Remove exact registration handle |
| Tokio task | Cancel and await JoinHandle |
| Guarded process tree | Request guard shutdown, let it settle the contained group, then reap the guard |
| Watcher/subscription | Unregister listener before producer shutdown |
| Local MCP connection | Close protocol session, terminate and reap its direct process group; no helper process is registered |
| LSP manager | Send shutdown, request guard shutdown, and reap all children |
| Background job supervisor | Cancel or settle according to job policy, then join |
Drop remains a last-resort safety net and is not accepted as proof of graceful unload.
First-party component crates may not call process spawning, tokio::spawn, or global registration APIs outside an ownership adapter unless the code documents an explicit process-lifetime exemption. Third-party executable plugins do not run as Rust dynamic libraries. They use a contained process protocol or the capability-restricted WASI Component Model host documented in plugins.md.
Product migration
Runtime and profiles
- Replace
MountContextwithPrepareContextandEffectScope. - Store component definitions so a failed transition can restore the previous composition.
- Publish runtime inventory and lifecycle diagnostics.
- Preserve child-scope inheritance, shadowing, reverse teardown, and atomic service publication.
- Preserve
ProfileBundleandHarnessProfileas the composition format.
Turn host
Move product capabilities behind typed component services. The migration order is:
- agent driver and tool manifest/contributions;
- background job and execution ownership;
- MCP and LSP lifecycle;
- provider registry and authentication-bound client construction;
- hooks, memory/reflection, commands, and client projections;
- server and ACP route registrations.
TurnHost remains a composition root and durable turn facade, but it does not privately create resources whose lifetime differs from its runtime.
TUI
drive_turnsowns theTurnHostand always executesshutdownbefore returning.- TUI exit closes producer channels or sends explicit stop signals, then awaits turn, MCP, LSP, editor, cancellation, and history workers under bounded deadlines.
- Host replacement prepares the candidate, shuts down the current host, and installs the candidate only after shutdown succeeds.
- A failed or uncertain shutdown is visible in the transcript and prevents a second composition from claiming the same resources.
- Session remount retains the physical terminal but not the previous session's live capabilities.
Extensions
Agent/workflow/skill contributions remain declarative catalog data. A static package may additionally declare executable tools backed by one WASI component or contained process. Their lifecycle is coupled to host composition:
- registry creates a candidate state without marking it running;
- the host resolves and prepares the candidate composition;
- executable hosts initialize as deferred profile effects;
- the old host shuts down;
- the candidate host commits and publishes plugin routing;
- only then does the registry publish
Runningand advance the scope-local generation.
Failure leaves the previous registry state and composition together. Generation is scoped by workspace rather than global to the process.
Process-local extension_define packages cannot declare runtime code. Executable packages must be installed statically so artifact provenance and package-relative paths are fixed before a host starts.
Implemented result
The native foundation and critical product boundaries are now in place:
Component::prepare,PrepareContext, deferred effects, reverse asynchronous cleanup, timeout handling, restoration, and lifecycle snapshots replace the old mount/closure API without a compatibility facade.- Parent shutdown is child-first. Parent recomposition refuses a live child scope; it does not claim an atomic transition while a stale consumer remains.
TurnHostprofile activation is side-effect-free until open, shutdown is fallible, and model/agent/MCP replacement stops the old host before starting the candidate.- TUI input, turn, MCP, LSP, editor, cancellation, and history workers have explicit stop paths and are joined under bounded deadlines. Session remount keeps the terminal but not the old host.
- Background child/product jobs are owned by a process/workspace supervisor, so remount cannot detach a task that still writes durable state. Final command exit cancels and joins the supervisor.
- Reflection's nested review task is cancelled with its returned owner. Tool interruption joins the cancelled invocation. The obsolete unowned tool-detach signal was removed in favor of
BackgroundExecutionService. - MCP remote shutdown reaches protocol
DELETE; local stdio readers, refresh, stderr, and child supervision have explicit owners. Resident local transports use one signal-driven guard per payload on Linux; owners never hard-kill that guard before it has settled the payload group. - Dynamic extensions separate committed and desired state, use scope-local revisions and active-consumer leases, and commit only after a reserved candidate host starts. TUI and server entry paths both use that transaction.
- Static executable packages register deferred runtime effects. WASI components receive only explicit workspace/network/environment grants plus fuel, memory, and wall-time budgets. Process plugins must declare
host.full, speak bounded JSON-RPC over stdio, and are stopped and reaped with the profile. Process-tree ownership is a lifecycle guarantee, not a sandbox: hostile process plugins require an external OS/container trust boundary. - Runtime tools use the native permission, strict-HITL, replay, concurrency, and UI-intent pipeline. Routing is withdrawn before reverse-order shutdown; a lost response or cleanup failure becomes
Uncertain. - Native composition now has coordinated typed and named planes. Named descriptors carry a validated key, contract, provenance, owner, runtime scope, monotonic generation, and availability; duplicate local keys fail before any effect starts, child scopes shadow and reveal parent descriptors, and stale generations are detectable without putting executable values in a string map.
- The production profile now exercises that plane: extension Tool objects remain in the typed
ToolContributionsservice while their provider-visible schemas are published as named contracts, and the typed orchestration snapshot transactionally publishes Agent Profile, Workflow Template, and source-scoped Skill descriptors. Provider Attempt records reuse the same canonical Tool schema identity calculation. - Runtime/component state and cleanup diagnostics are projected through frontend-neutral snapshot values.
This does not introduce a Rust dylib ABI or arbitrary in-process native code. Trusted compiled Rust components remain build-time composition; runtime-loaded code uses WASI or a contained process.
TDD and acceptance matrix
Tests are added before the behavior they require.
zuno-runtime
- candidate effects do not start during prepare;
- candidate services remain invisible until every effect starts;
- effects stop in reverse registration and component order;
- stop waits for actual task/process completion;
- explicit stop failure becomes
Uncertain; - a hanging stop is bounded and becomes
Uncertain; - replacement never starts a candidate before the old exclusive effect stops;
- a failed candidate start restores the previous composition;
- failed restoration leaves no service published and records diagnostics;
- profile replacement re-prepares a consumer against the new provider;
- unmount re-prepares a consumer against the revealed provider;
- parent shutdown closes children first;
- inventory exposes active, stopping, failed, uncertain, and closed states;
- repeated shutdown is idempotent.
Production entry paths
- a real default profile still resolves driver and tool services;
- TUI exit awaits
TurnHost::shutdown; - model/agent/MCP host replacement shuts down the previous host exactly once;
- session remount closes the old host before the next composition starts;
- TUI LSP shutdown reaches
Manager::shutdown; - TUI MCP shutdown closes remote sessions rather than only dropping transports;
- background jobs cannot outlive the process/workspace supervisor that owns their write authority.
Extensions
- a failed host preparation does not publish
Running; - a failed old-host shutdown does not publish the candidate state;
- run/stop/undefine change only the matching workspace generation;
- successful commit updates catalog and lifecycle state together;
- restart still drops process-local definitions;
- static and dynamic packages use the same validated catalog contribution path.
- process-local definitions reject executable runtimes;
- WASI initialization, invocation, guest error, malformed metadata, timeout, cancellation, resource limits, and shutdown are bounded;
- process plugins negotiate the protocol, bound frames and stderr, redact known secrets, stop the process tree, and mark lost replies uncertain;
- plugin routing is unavailable before all hosts initialize and is withdrawn before reverse cleanup;
- add/update/remove package installation is transactional and rejects symlinks.
Full gates
Focused tests run after each red/green cycle. Delivery requires complete success from:
cargo fmt --all --check
cargo test -p zuno-runtime
cargo test -p zuno-harness
cargo test -p zuno-extension
cargo test -p zuno-cli
cargo check --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
git diff --check2
3
4
5
6
7
8
PTY tests that exercise TUI exit/remount are mandatory evidence; source-string assertions alone are not acceptance.
Delivery sequence
- Lifecycle specification and failing runtime tests.
- Native lifecycle kernel and profile migration.
- TUI/TurnHost graceful ownership and PTY tests.
- Extension activation transaction and scope-local generation.
- Critical product resource adapters and lifecycle projection.
- Architecture documentation, DSH adoption ledger, full gates.
Delivery uses logical commits. Existing user-owned engineering-note changes remain outside every commit.