The KDCube Application Lifecycle: Load, Reload, Build, and Evict
A KDCube app is resolved as a generation, invoked with fresh request context, and cut over cleanly when code, configuration, or UI changes.
A KDCube app is not a daemon that owns one permanent Python object. Proc resolves an app generation, loads its entrypoint, binds the current request, applies effective properties, invokes one declared surface, and finalizes the work. Reload changes the generation used by new invocations. It does not rewrite code underneath a coroutine already running.
An AI app lifecycle becomes difficult to operate when four different actions are all called "restart": changing a feature property, selecting a new Git revision, rebuilding a widget, and rebuilding the platform image.
KDCube gives those actions different boundaries. Properties can refresh on live app instances. App descriptors and code move through cache invalidation and generation cutover. UI artifacts rebuild from content signatures. Platform source or image changes use platform refresh. In-flight work continues under the generation it already loaded.
This article follows an app from discovery to invocation and then through each kind of change. It does not repeat the package structure or first integration steps covered by Build an AI App with KDCube.
Current source and commands still use bundle in literal contracts such as @bundle_entrypoint, bundles.yaml, singleton, and kdcube bundle reload. Builder-facing prose calls that deployable unit an app.
00Implementation highlights
| Concept | The takeaway |
|---|---|
| Proc owns the lifecycle | Discovery, app loading, hooks, route invocation, background-job dispatch, UI build coordination, and eviction happen in proc. |
| Invocation state is request-local | Actor, user, authority, communicator, conversation, and turn are rebound for every call, including on a reused singleton. |
| Singleton is an optimization | It is worker-local, concurrent, ephemeral reuse of one loaded app spec, not a durable service or serialization guarantee. |
| Change has several boundaries | Properties refresh, descriptor save, app reload, UI rebuild, venv rebuild, and platform refresh are distinct operations. |
| Reload is a generation cutover | New invocations resolve the updated app; already-running invocations finish with the generation they loaded. |
| UI is content-addressed | Proc builds missing or stale UI under a shared lock, publishes atomically, and leaves no permanent failure marker after a failed build. |
01The smallest honest lifecycle
For one API request, widget operation, MCP call, chat turn, Data Bus message, or background job, the conceptual path is:
configured app registry -> resolve source + module + app generation -> discover entrypoint and surface metadata -> instantiate app or reuse worker-local singleton -> bind current request, actor, user, authority, and routing context -> load and merge effective app properties -> invoke the declared async surface -> run surface/turn finalization -> release request-local context
The app instance is not the request. Even when an instance is cached, the request context is rebuilt and rebound.
The runtime expects every platform-invoked surface and every I/O chain behind it to remain async. A synchronous database, HTTP, filesystem, Redis, subprocess, sleep, or lock call inside async def blocks the shared proc event loop and delays other apps and users.
02Discovery, load, and the one-time hook
Proc starts from the configured app registry. It resolves a Git or local source, imports the declared module, discovers decorators and interface metadata, and constructs the entrypoint.
on_bundle_load() is the one-time initialization hook for one app in one proc process and tenant/project context. It is suitable for deterministic, idempotent preparation:
- verify or create app-owned tables and indexes;
- materialize a local search index from a source signature;
- prepare a repository mirror or shared read-only assets;
- reconcile an app-owned registry;
- ensure a hot public-content projection exists.
It is not a place for request-local actor, user, conversation, turn, or communicator state. Several proc workers can load the same app, so mutations of shared PostgreSQL, Redis, object storage, or EFS still need a lock at that shared scope.
on_bundle_load() means once per relevant process lifecycle, not once for the entire fleet and not once forever. The operation must remain safe after worker restart, scale-out, cache eviction, or a new app generation.
03Non-singleton and singleton apps
The safest default mental model is stateless per invocation.
Without singleton reuse, the runtime constructs an app instance for the invocation. Durable data remains in properties, secrets, PostgreSQL, Redis, app storage, or another explicit owner.
With singleton: true, one proc worker may reuse an instance associated with a loaded app spec. That can avoid repeated construction for crawler-heavy public content, expensive client setup, or other justified cases. It does not change the durability model.
| Singleton fact | Operational consequence |
|---|---|
| Cache is in one proc worker | another worker has another instance |
| Key is the loaded app spec, not merely app id | source/module changes create a different generation |
| The same object can serve several users and surfaces | request state cannot live in instance fields |
| Calls can overlap | singleton does not serialize app methods |
| Worker restart drops the object | memory is not authority |
| Request context is rebound | self.comm, actor, user, conversation, and turn must resolve from the current invocation |
Long-lived shared clients can live on a singleton only when they are safe for concurrent use and do not capture a previous request's identity. A per-user client, mutable graph state, selected conversation, or old communicator reference cannot.
04What happens on every invocation
Before the app surface runs, proc reconstructs the current execution context. That includes the effective tenant/project, app, actor, user, authority, routing, and surface metadata required by the call.
The runtime then applies effective app properties:
code defaults deep-merged with descriptor / Bundle Admin properties plus request-scoped bundle_call_context where a supported feature consumes it
If the effective app properties changed on a cached instance, on_props_changed(previous_props, current_props, ...) can reconcile long-lived side effects. It is not a second constructor and should not perform slow one-time installation that belongs in on_bundle_load().
Examples:
- update a reusable client when its non-secret endpoint changed;
- replace a timer or local policy snapshot;
- rebuild a model service when platform-interpreted model properties changed;
- close a resource no longer enabled by the new properties.
The current invocation sees one coherent effective snapshot. Application code should read through self.bundle_prop(...), not poll raw descriptor files.
05Six kinds of change, six application moments
The word "hot reload" hides important differences. The runtime applies changes at the boundary appropriate to the changed thing.
| What changed | Authority/change path | When new behavior appears |
|---|---|---|
| Non-secret app property | supported helper or Bundle Admin persists authority, updates Redis cache, publishes props update | refreshed cached instance or next invocation; on_props_changed runs only when effective props differ |
| App secret | configured secrets provider | a fresh helper read sees the provider value; clients/config objects that captured an old secret must be reconstructed |
App registry/source fields (repo, ref, subdir, path, module, singleton) | Bundle Admin Save or descriptor-authority replay | workers evict the old generation; new invocations resolve the saved generation |
| Python code or decorator metadata | app reload/cache eviction | next invocation imports/discovers the updated generation |
| UI source | source signature checked on main-view/widget HTML entrypoint | current artifact is served when signature matches; stale/missing output builds before serving |
@venv requirements | requirements.txt content hash | next decorated call lazily rebuilds that app venv |
Secret rotation has no universal props-style notification. Code that calls the async secret helper for each operation sees the current provider value. A singleton client constructed with a secret keeps what it captured until app code explicitly refreshes it or the app generation is reconstructed. On the chat path, provider keys used to construct a turn's model configuration are resolved for the next turn.
Surface behavior can therefore change without code reload when it is intentionally property-driven: feature gates, visibility policy, model routing, cron configuration, MCP connections, UI config, or another declared runtime knob. Changing an API implementation, decorator, import, or module still changes code and needs app eviction/reload.
06Save, reload, build, and refresh are not synonyms
Bundle Admin Save
When an administrator changes app registry fields or configuration and presses Save, the platform:
- persists the configured app authority;
- updates the active Redis registry;
- publishes the app update;
- makes proc workers evict the changed app's imported code, singleton, manifest, and static-entrypoint state as required;
- returns without running npm, Vite, or another UI build command.
For a changed Git ref, Save is already the registry mutation and invalidation operation. Pressing a separate Reload button immediately after it is not the normal second half of Save.
Reload app
Reload rereads/reapplies the existing authority and evicts the selected app from proc caches. Use it when code or descriptors changed outside Bundle Admin, or when explicit import/manifest/static-entrypoint invalidation is needed.
Reload does not synchronously build every UI and does not rerun the completed startup preload pass.
UI build
The next startup preload or main-view/widget HTML request resolves the active generation, computes the UI signature, and builds only when the shared artifact is missing or stale.
Platform refresh
Platform refresh is for platform source, images, dependencies, and runtime services. It is not the normal inner loop for editing one app.
| Operator intent | Correct boundary |
|---|---|
| Change a live app property | Bundle Admin or supported property helper |
| Select another Git revision | Save the app registry change |
| Load edited local Python/decorators | kdcube bundle reload <app-id> ... |
| Rebuild edited UI source | request/reload the HTML entrypoint; signature decides |
| Rebuild an app-specific venv | change requirements; next decorated call rebuilds |
| Change KDCube platform code/image | platform refresh/build/restart flow |
07A reload does not rewrite an in-flight request
Suppose request A began under generation G1 while an administrator saves G2.
request A: resolve G1 -> execute G1 ----------------------> finish G1
admin: Save G2 -> publish update
|
workers: evict G1 caches
|
request B: resolve G2 -> execute G2
Request A continues with the code and effective context it already loaded. Request B sees G2 after invalidation and resolution. This is generation cutover, not live mutation of Python frames.
That property keeps the change understandable. The runtime does not promise that every concurrent request flips at one global nanosecond. It promises that new invocations use the updated generation after the relevant invalidation boundary, while old invocations are not half-rewritten.
08UI build is a coordinated artifact lifecycle
Widgets and main views are built by proc. They do not build in the generated code executor or the isolated runtime.
The source/build declaration belongs to effective app properties. The build signature includes the component kind, component source tree and path, build command, bundle delivery id, and signatures of declared shared UI sources. Proc copies/assembles build input in a worker-local work area, while the completed output, signature, and lock live under the configured shared app storage.
Several workers may discover stale output concurrently. Only the lock owner builds; waiters re-check the signature and use the completed artifact.
The build task is shielded from a canceled browser request. A timeout or administrative cancellation terminates and reaps the subprocess tree, writes no successful signature, and releases the lock. A later qualifying request can retry. If the worker dies, its lock heartbeat stops and expiry permits another worker to recover.
Main views and widgets have separate outputs and signatures. Declaring a main view as an app-hosted website adds routing/catalog behavior; it does not create a second UI compiler.
09Startup preload warms generations, it does not watch forever
When bundles_preload_on_start is enabled, each proc worker joins one collaborative preload generation during startup. Redis claims distribute apps across workers, while shared signatures and locks prevent duplicate UI builds.
The preload generation includes the source/module/singleton facts and resolved Git commit. A deterministic new revision therefore becomes a new generation.
Startup preload is not a continuously running reconciler:
- saving an app while proc is already running invalidates that app;
- opening its main view/widget can warm the changed generation;
- restarting proc starts another full collaborative preload pass;
- Reload app invalidates the app but does not replay startup preload itself.
Health output exposes preload readiness and per-app status so an operator can distinguish "registered," "loaded," "UI warm," and "failed to preload."
10Git is the deployable source; local paths are the development seam
Operator-supplied apps in non-local deployments normally resolve from Git:
repo: https://github.com/example/acme-apps.gitref: <immutable release ref or commit>subdir: apps/reportingmodule: entrypoint
Built-in platform apps use their reserved platform delivery path. For a Git-delivered app, an immutable ref makes the generation reproducible. Changing the selected ref through Bundle Admin or the descriptor authority creates the normal Save and eviction journey described above.
Local path plus module is for local development only. It points proc at host-edited source mounted into the local runtime:
path: /bundles/acme-apps/apps/reportingmodule: entrypoint
After local Python or decorator changes, reload that app. UI-only source edits are detected by the UI signature on the next HTML entrypoint request; reload remains the escape hatch when Python defaults, decorators, manifests, imports, or other process-local state also changed.
Do not mix path with repo, ref, or subdir on one app item. A source is either the local development seam or the Git deployment definition.
11The lifecycle review
Before adding state or operational behavior to an app, verify:
- Is this data durable outside the Python instance?
- Is initialization deterministic and idempotent in every proc worker?
- Does shared mutation use a lock at the actual shared store?
- Can the same singleton object receive two users concurrently without leaking request context?
- Is the change a property refresh, secret reconstruction, registry Save, app reload, UI signature rebuild, venv rebuild, or platform refresh?
- What happens to an invocation already running under the old generation?
- Is UI output published atomically only after a complete build?
- Can a failed build or dead worker be retried without a permanent poison marker?
The result is a lifecycle that can scale across workers without turning every change into a fleet restart.
Read the contracts
Platform documentation:
- App lifecycle
- UI components lifecycle
- App properties and secrets lifecycle
- App platform integration
- Configure and run an app
- App descriptor
- Per-app venv
Related public articles:
The Markdown file beside this page is the approved indexing source. Literal platform contracts retain names such as bundle_id, bundles.yaml, and SDK bundle decorators.