KDCube
← Engineering
KDCube Engineering · Deep Dive

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.

18 July 2026Engineering15 minExperienceWorkshop Manual
app lifecyclehot reloadUI buildsproc workersKDCube app reloadBundle Adminapp generation

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 and can be reconciled before page load. 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

ConceptThe takeaway
Proc owns the lifecycleDiscovery, app loading, hooks, route invocation, background-job dispatch, UI build coordination, and eviction happen in proc.
Invocation state is request-localActor, user, authority, communicator, conversation, and turn are rebound for every call, including on a reused singleton.
Singleton is an optimizationIt is worker-local, concurrent, ephemeral reuse of one loaded app spec, not a durable service or serialization guarantee.
Change has several boundariesProperties refresh, descriptor save, app reload, UI rebuild, venv rebuild, and platform refresh are distinct operations.
Reload is a generation cutoverNew invocations resolve the updated app; already-running invocations finish with the generation they loaded.
UI build and delivery are separateProc builds missing or stale UI under a shared lock. The rollout-controlled deployed path also publishes an authorization manifest so a current artifact can be served without loading the app on that request.

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.

One KDCube application invocation A seven-station process rail from registry through finalization. Request context attaches only at bind and remains through finalization. Durable state remains outside application memory. WORK ORDER 01 · ONE INVOCATION Resolve a generation. Bind a request. Invoke one surface. The application object is never the request, even when proc reuses a worker-local singleton. 01 Configured registry authority 02 Resolve generation source + spec 03 Discover surfaces decorators 04 Instantiate or reuse worker-local 05 Bind current request context joins 06 Apply properties one snapshot 07 Invoke + finalize async surface DETACHABLE REQUEST CONTEXT actor · user · authority · routing · conversation turn · communicator DURABLE STATE explicit stores, never instance memory PostgreSQL · Redis · app storage · artifacts · conversation stores
The app object may be reused; request context belongs to one invocation, while durable state stays in owned stores.

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.

The optional async on_app_deploy() hook has a different scope. Proc invokes it once for one shared app generation under a distributed generation fence, after process-local loading and before deployment state becomes current. It is suited to idempotent publication work that should not run once per worker. The static-widget deployment coordinator runs this hook, reconciles UI output, and then publishes a small authorization manifest atomically. Request-local identity does not belong in either hook.

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 factOperational consequence
Cache is in one proc workeranother worker has another instance
Key is the loaded app spec, not merely app idsource/module changes create a different generation
The same object can serve several users and surfacesrequest state cannot live in instance fields
Calls can overlapsingleton does not serialize app methods
Worker restart drops the objectmemory is not authority
Request context is reboundself.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 changedAuthority/change pathWhen new behavior appears
Non-secret app propertysupported helper or Bundle Admin persists authority, updates Redis cache, publishes props updaterefreshed cached instance or next invocation; on_props_changed runs only when effective props differ
App secretconfigured secrets providera 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 replayworkers evict the old generation; new invocations resolve the saved generation
Python code or decorator metadataapp reload/cache evictionnext invocation imports/discovers the updated generation
UI sourcestartup or app-update reconciliation; the legacy HTML route remains a fallbackcurrent artifact is reused when its signature matches; stale/missing output builds under one shared lock before publication or fallback serving
@venv requirementsrequirements.txt content hashnext 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.

Six application change boundaries Six operation cards distinguish property refresh, secret resolution, registry save and eviction, Python reload, UI deployment reconciliation and publication, and virtual environment rebuild. WORK ORDER 02 · CHANGE CONTROL Six changes. Six application moments. Use the boundary owned by the thing that changed; “restart” is not one operation. OP 10 · PROPERTIES Refresh persist authority → cache → publish current/next invocation sees one snapshot on_props_changed only when values differ OP 20 · SECRETS Resolve again provider read → reconstruct client no universal props-style notification fresh helper read sees provider value OP 30 · REGISTRY Save + evict persist spec → update registry → notify new invocations resolve saved generation Save does not run npm OP 40 · PYTHON Reload + import evict caches → discover decorators next invocation imports new generation in-flight work stays on old generation OP 50 · UI Reconcile + publish generation → signature → shared lock artifact + policy manifest legacy request path remains fallback OP 60 · VENV Hash + lazy rebuild requirements hash → isolated environment next decorated call checks content hash app-specific dependency boundary THE RULE No single restart verb covers these six contracts.
Properties, secrets, registry state, Python, UI, and venvs have separate refresh boundaries.

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:

  1. persists the configured app authority;
  2. updates the active Redis registry;
  3. publishes the app update;
  4. makes proc workers evict the changed app's imported code, singleton, manifest, and static-entrypoint state as required;
  5. 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

In legacy, startup preload or the next main-view/widget HTML request resolves the active generation, computes the UI signature, and builds only when the shared artifact is missing or stale. In shadow and deployed, startup and live app-update events reconcile the generation and publish deployment state; the Save or Reload HTTP request still returns without waiting for npm or Vite.

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 intentCorrect boundary
Change a live app propertyBundle Admin or supported property helper
Select another Git revisionSave the app registry change
Load edited local Python/decoratorskdcube bundle reload <app-id> ...
Rebuild edited UI sourcereload/refresh the app generation; reconciliation checks the signature, while the legacy HTML route remains a fallback
Rebuild an app-specific venvchange requirements; next decorated call rebuilds
Change KDCube platform code/imageplatform 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.

Application generation cutover An in-flight request continues on generation G1 while an administrator saves G2. After workers evict G1 caches, a new request resolves and runs G2. WORK ORDER 03 · GENERATION CUTOVER New work moves. In-flight work is not rewritten. SAVE G2 REQUEST A resolve G1 execute G1 finish G1 REQUEST B resolve G2 execute G2 publish update → evict G1 caches Generation cutover, not frame mutation G1 remains coherent for request A; G2 becomes the generation resolved by request B.
In-flight work finishes on G1; new work resolves G2 after cutover.

08UI build and delivery are coordinated separately

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, lock, and generated policy manifest live under the configured shared app storage.

UI deployment and authorized delivery are coordinated separately Startup or an app-update event resolves a deployment generation and claims a shared generation fence. Proc runs on_app_deploy, then reconciles each widget UI signature, reusing a current artifact or building under one shared lock. It publishes the policy manifest only after the hook and UI reconciliation succeed. A browser request serves through a current manifest after proc authorization; missing or stale state uses the legacy fallback, while an authorization denial never falls through. WORK ORDER 04 · UI DEPLOYMENT Fence a generation. Reconcile UI. Authorize each request. Deployment work leaves page load; proc remains the policy boundary for every protected file. TRIGGER startup / app update source or policy changed GENERATION resolve deployment identity source · props · runtime release FENCE shared claim one owner; retryable OWNER prepare once for this generation APPLICATION DEPLOY HOOK run async on_app_deploy under the generation fence idempotent app preparation; no request-local identity FAILURE RULES nothing becomes current timeout / cancel reaps the subprocess tree, writes no successful signature or manifest; a dead owner’s fence expires and another proc can retry UI RECONCILIATION one shared build lock per missing or stale output compute and re-check the widget UI signature reuse a current artifact without running a build otherwise run npm / Vite in a worker-local area publish completed output and signature atomically CURRENT DEPLOYMENT atomically publish the widget policy manifest artifact path + generation + visibility and grant policy BROWSER REQUEST current manifest + proc authorization → serve the built file missing/stale → legacy fallback · authorization denial never falls through PREPARE ON GENERATION CHANGE · ENFORCE POLICY ON EVERY REQUEST
Reconcile under one generation fence, publish atomically, then serve through the current authorization manifest.

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.

The request path is rollout-controlled by platform.services.proc.bundles.static_widget_delivery_mode:

ModeWhat is preparedWhat serves the request
legacyestablished startup preload onlythe workflow-resolving, source-signature-aware route
shadowUI plus deployment/policy manifestthe legacy route, with X-KDCube-Widget-Delivery: legacy-shadow
deployedthe same UI and manifestthe current manifest after proc enforces app and widget policy; missing/stale state falls back to legacy

This remains a proc-authorized file path, not a public CDN bucket. Arbitrary roles, user types, and authority grants still apply before a protected widget file is returned. Local deployments use local bundle storage; shared deployments can use their existing shared filesystem.

The first warm local comparison did not establish a meaningful latency gain. Across 100 requests per mode, the means differed by 10.7 ms while each run's standard deviation was 46-48 ms and the ranges overlapped. The immediate value is a shorter, bounded request path and a clean publication boundary for later delivery work, not a speed claim.

09Startup preload and live deployment reconciliation

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.

In legacy, 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.

shadow and deployed add a per-app reconciler to the existing update path. They imply startup preload and react to app source, descriptor-property, and explicit reload events. A shared generation fence keeps multiple proc workers from publishing the same generation concurrently. Publication becomes current only after on_app_deploy(), the UI build reconciliation, and the policy-manifest write succeed; until then a request can use the legacy fallback.

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:

descriptorYAML
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:

descriptorYAML
path: /bundles/acme-apps/apps/reportingmodule: entrypoint

After local Python or decorator changes, reload that app. In legacy or shadow, the serving route still checks the UI signature on an HTML entrypoint request. deployed deliberately does not poll source trees on every browser request: reload or runtime refresh starts a new deployment generation and publishes its current manifest. Reload also remains the boundary when Python defaults, decorators, imports, or other process-local state 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:

  1. Is this data durable outside the Python instance?
  2. Is initialization deterministic and idempotent in every proc worker?
  3. Does shared mutation use a lock at the actual shared store?
  4. Can the same singleton object receive two users concurrently without leaking request context?
  5. Is the change a property refresh, secret reconstruction, registry Save, app reload, UI deployment reconciliation, venv rebuild, or platform refresh?
  6. What happens to an invocation already running under the old generation?
  7. Is UI output published atomically only after a complete build?
  8. Can a failed build or dead worker be retried without a permanent poison marker?
  9. Does widget delivery preserve the same user-type, role, and authority-grant checks after the artifact is prepared?

The result is a lifecycle that can scale across workers without turning every change into a fleet restart.

Read the contracts

Platform documentation:

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.

KDCube Engineering
18 · 07 · 2026