One App Can Be Preparing While the Processor Stays Ready
KDCube now prepares every app under its own supervised lifecycle. A slow checkout, hook, index, or UI build makes that app temporarily unavailable without making an otherwise healthy processor look dead.
A processor can be alive while one of its apps is still getting ready. Its event loop is running. Redis and PostgreSQL are connected. Other apps can answer. The slow app may be checking out source, rebuilding an index, waiting for a recoverable shared lock, or compiling a UI.
Previously, one registry-wide preload pass made those facts indistinguishable. The processor reported not ready until the slowest configured app finished. An orchestrator could then replace a healthy process because an app was legitimately preparing, interrupt the work, and make recovery take longer.
The correction was not to skip app initialization or hide it in an unowned background task. KDCube now gives every app a supervised lifecycle and separates four availability facts that used to collapse into one.
Preparation remains a first-class lifecycle
At processor startup, KDCube reads the authoritative app registry and computes one desired generation for every app. A generation is an internal fingerprint of the source, module, effective properties, and runtime resource schema that affect prepared behavior. It is not an app version shown to a caller.
The processor starts one strongly owned task per app, under bounded concurrency, and continues starting its core turn, event, job, and HTTP machinery.
The ownership distinction inside that rail matters.
on_bundle_load() runs in every processor process that loads the app.
It prepares process-local clients, caches, repositories, or indexes. A shared
completion marker cannot prove that another process ran its local hook.
on_app_deploy() has the shared scope. It publishes idempotent
resources for one desired app generation, coordinated by signatures and
distributed locks. Static UI compilation is one participant in that shared
deployment phase, not the definition of readiness.
Only after both scopes complete does the local process mark that app generation ready. A late completion for an older generation cannot satisfy a newer desired state.
Four facts, four owners
The descriptor chooses only the aggregate-readiness answer:
- id: reports@1-0
path: /bundles/apps/reports@1-0
module: entrypoint
service:
readiness: required
independent is the default. An independent app does not hold
aggregate /health while it prepares. Its own doors are still closed
until it is ready.
required means normal processor traffic depends on that app. While
it prepares, /health returns 503;
/health/live remains a liveness fact and stays healthy. The app's own
doors are closed in both modes.
Independent describes aggregate health policy, not permission to call an unready app.
Doors read state; they do not repair it
After resolving an app id and before loading or invoking app code, each application surface reads the committed readiness state. The request never starts source resolution, lifecycle hooks, npm, Vite, or deployment repair.
{
"type": "application_not_ready",
"application_id": "reports@1-0",
"state": "preparing",
"retryable": true
}
The internal generation, source path, credentials, and preparation error stay out of the public response. Authenticated operator diagnostics retain the desired and ready generations, attempts, timestamps, retry timing, and a bounded error.
- REST, UI, sites, and public content return app-scoped
503. - MCP carries the same fact in JSON-RPC error data.
- Chat releases its unstarted queue claim before visible effects.
- Scheduled work stays inactive until the READY callback reconciles it.
- Data Bus work remains unacknowledged for a later claim.
- Admin status, configuration, reload, and retry stay available.
This keeps inventory honest. The app is configured and discoverable; it is temporarily unavailable for execution. Nothing starts a second preparation task because a caller arrived at the wrong moment.
Failure belongs to the app that failed
The lifecycle supervisor retains every task reference and observes every
completion. A failed app moves to retrying and uses bounded
exponential backoff. Another app continues serving. A source or property update
computes a new desired generation, invalidates only that app's local state, and
supersedes only its old preparation task.
Request cancellation has no ownership relationship to preparation. A browser disconnect cannot cancel a shared UI build or app deployment. Explicit supersession and processor shutdown can, and both paths cancel and reap the owned work.
For UI work, the existing lower-level guarantees remain: worker-local source
and node_modules, shared locks and output, lock heartbeat and TTL
recovery, npm/Vite in a dedicated process group, atomic artifact publication, and
cleanup after the signature is published and the lock is released.
One runtime contract, different deployment adapters
- Process or container replacement probes
/health/live. - Traffic readiness probes
/healthwhen required-app policy should control the processor as a whole. - App-specific calls rely on the application door.
- Monitoring and authenticated diagnostics explain state without redefining it.
The same state model works under Docker Compose, Kubernetes, and ECS. The deployment platform consumes the runtime's answers; it does not decide what application readiness means.
Highlights
on_bundle_load() and on_app_deploy() keep distinct scopes.service.readiness controls aggregate readiness only.