Platform
Ingress (chat-ingress)
Handles all inbound traffic. Authenticates users, enforces rate limits, validates the app and environment scope, enqueues the task, and opens an SSE stream back to the client. Your app rarely needs to know about this — the platform wires it up automatically.
Processor (chat-proc)
Dequeues tasks, loads your app singleton, and calls execute_core(). This is also where the ReAct subsystem runs, where apps can launch ISO runtime work, and where the Operations API lives for app widget UI, main UI, and other app-owned frontends.
Task Orchestrator
Runs inside chat-proc. It balances interactive chat tasks from Redis Lists with ready background work from Redis Streams, creates normal app runtime context, and invokes @on_job handlers for durable app-owned work.
bundle_id and /bundles/... as compatibility identifiers.Object, Event, and Data Planes
Named-service registry
Maps object namespaces to app-provided operations such as search, schema, action, materialization, and block policy hooks.
Event bus
Carries live UI-visible events, recorded turn events, tool lifecycle steps, and external event envelopes across Ingress, Proc, and connected clients.
Data bus
Carries typed subsystem payloads and surface coordination messages that are not plain chat deltas, for example search results, context pins, and widget state events.
Event Bus and Data Bus Architecture
KDCube uses two related communication planes. The event bus carries time-ordered happenings that clients, recorders, and timelines may observe. The data bus carries typed subsystem payloads and coordination messages such as named-service search results, context pin envelopes, scene commands, and widget state updates.
App / ReAct runtime / integration / widget
emits event or data payload
|
v
Processor
applies app event firewall, records selected events, resolves provider policies
|
v
Redis / runtime channels
event bus: live + recorded event envelopes
data bus: typed subsystem payloads and surface coordination
|
v
Ingress
fans out to authenticated SSE / Socket.IO clients
|
v
Client surfaces
chat steps, artifacts, scene, canvas, memory/task widgets, custom app UI
| Plane | Payload | Examples | Consumer |
|---|---|---|---|
| Event bus | Timestamped event envelopes | chat deltas, tool call/result lifecycle, external event envelope, recorded app events, turn completion | Chat UI, timeline, event sinks, audit views |
| Data bus | Typed subsystem data | named-service search results, context pins, scene open command, canvas patch notification, widget state payload | Scene host, canvas, widgets, custom app surfaces |
SSE Streaming Flow
Modules
Platform Architecture — Detail
Services
| Service | Port | Role | Required? |
|---|---|---|---|
| web-proxy | :443 / :80 | TLS termination, token unmasking, routing | Required |
| chat-ingress | :8010 | Auth, SSE/Socket.IO gateway, task enqueueing | Required |
| chat-proc | :8020 | App execution, integrations REST API, task orchestrator, background job stream consumer | Required |
| web-ui | :80 | SPA frontend | Required |
| kdcube-secrets | internal | Local dev secrets helper used by the descriptor-driven runtime | Required (local) |
| metrics | internal | Autoscaling metric export (CloudWatch) — not needed for single-node | Optional |
| proxylogin | internal | Delegated auth token exchange | Optional |
| clamav | internal | Antivirus scanning for file attachments | Optional |
| exec (on-demand) | — | Ephemeral Docker/Fargate container for isolated code execution | Optional |
Routing
| Path Pattern | Routes To |
|---|---|
/sse/*, /api/chat/*, /admin/* | chat-ingress |
/api/integrations/* | chat-proc |
/auth/* | proxylogin (delegated auth only) |
/* | web-ui |
Processor Architecture
The chat-proc service is the execution side of the platform. After ingress admits and enqueues a request, the processor claims it, loads the target app, executes the workflow, and streams results back through the relay communicator.
App State: What Lives Where
| Data class | Live authority today | Operational note |
|---|---|---|
| Deployment-scoped app props | Mounted writable bundles.yaml when present; Redis is the proc runtime cache; grouped descriptor docs are fallback only when no mounted file exists | Exported with app descriptors |
| Deployment-scoped app secrets | Configured secrets provider; in local secrets-file mode this is bundles.secrets.yaml | Exported with app secrets only |
| User-scoped app props | PostgreSQL <SCHEMA>.user_bundle_props | Operational user data, not descriptor state |
| User-scoped app secrets | Configured secrets provider; in local secrets-file mode this is secrets.yaml | Operational user data, not descriptor state |
This split is intentional: only deployment-scoped app state belongs to descriptor authority and app export. User-scoped state stays outside bundles.yaml and bundles.secrets.yaml.
When isolated execution runs in Docker or Fargate, chat-proc ships the descriptor authority to the supervisor as KDCUBE_RUNTIME_*_YAML_B64 payloads. The supervisor materializes those descriptors and uses the same settings/secrets APIs as proc; generated code receives only the filtered executor environment. App runtimes can set descriptor_payload_scope: active_bundle to filter bundles.yaml and bundles.secrets.yaml to the active caller app before packaging.
Task Queue Model
The processor uses Redis Lists as its task queue. Ingress pushes task payloads with LPUSH; processor workers claim them with BRPOPLPUSH, atomically moving the item from a ready queue to an inflight queue. This gives FIFO ordering within each lane.
Tasks are partitioned into user-type lanes — privileged, paid, registered, and anonymous. Workers rotate fairly across lanes so no single tier starves the others. Each claimed task is protected by a per-task Redis lock (SET NX EX) to prevent duplicate processing.
| Redis Key Pattern | Purpose |
|---|---|
{tenant}:{project}:...:queue:{user_type} | Ready queue (one per user-type lane) |
{tenant}:{project}:...:queue:inflight:{user_type} | Inflight queue (claimed but not yet complete) |
{LOCK_PREFIX}:{task_id} | Per-task dedup lock |
{LOCK_PREFIX}:started:{task_id} | Started marker — prevents auto-replay once execution begins |
Task Orchestrator And Background Job Stream
Interactive chat turns are only one kind of work. The processor also runs a sibling background job stream for ready work produced by cron scans, widget operations, admin actions, or app-specific schedulers. These jobs use Redis Streams, not the chat Redis List queue.
The stream does not decide when work is due. A producer first creates any durable app-owned domain record, then enqueues a ready-work envelope. The processor fairly polls chat work and background work, claims stream messages through a consumer group, builds a normal app runtime context, and invokes the app's async @on_job handler.
| Layer | Responsibility |
|---|---|
| Producer | Detect due work, create the durable domain record, choose work_kind, job_id, dedupe_key, metadata, and payload. |
| Redis Stream | Persist ready work, dedupe submissions, expose consumer-group claiming, and recover idle pending jobs with XAUTOCLAIM. |
| Processor | Route the envelope to the target tenant/project/bundle_id app slot, bind runtime context, invoke @on_job, and acknowledge only after success. |
App @on_job | Interpret work_kind, load app-owned records from payload, execute the job, and update execution/result state. |
The platform treats metadata and payload as routing/runtime context plus app-owned data. It does not understand app-specific job semantics. That boundary lets apps implement scheduled reports, task executions, mailbox processing, or other domain work without adding a new platform service for each job type. See the source design note: jobs-stream-README.md.
App Loader & Lifecycle
Processor workers load apps through a registry + singleton cache model:
- On startup (and on app-update broadcasts), the worker rebuilds its in-memory app registry.
- At request time, the registry resolves the app to a concrete path. Module/singleton cache keys are based on the resolved path.
- Built-in example apps are merged into the registry and copied to shared storage with a versioned path (
/bundles/{bundle_id}__{ref}__{sha}). - On update, loader caches are cleared. New requests use the new path; already-running turns continue on the previously loaded path.
This means app updates are zero-downtime — running work is never affected, and new work picks up the latest version automatically.
Execution Pipeline
Once a task is claimed, execution follows a fixed sequence:
- Receive — claim the task from the ready queue via
BRPOPLPUSHand acquire the per-task lock. - Validate — materialize
ChatTaskPayload, buildServiceCtxandConversationCtx. - Load app — resolve the target app through the registry, load/reuse the singleton.
- Execute — run the app handler under task timeout, accounting binding, lock renewal, and started-marker renewal. On ECS, scale-in protection is enabled for the duration.
- Stream results — the app emits events through the
ChatCommunicator; the relay forwards them via Redis pub/sub to ingress, which delivers them to the client over SSE.
On success the inflight claim is acked, conversation state moves to idle, and conv_status is emitted. On failure the conversation is set to error and the client receives a chat.error event.
State Management & Recovery
The processor distinguishes two recovery cases based on the started marker:
- Pre-start claims (lock expired, no started marker) — safe to requeue. The inflight reaper moves the item back to the ready lane.
- Started tasks (lock expired, started marker present) — not replayed. The conversation is set to
errorand the client is notified withturn_interrupted. This prevents duplicate side effects from partial execution.
The started marker intentionally outlives the claim lock so that a hard worker restart cannot accidentally let both leases expire and trigger an unsafe replay.
Continuation Mailbox
When a user sends a message to a conversation that is already executing, ingress stores it in a per-conversation ordered mailbox in Redis rather than the main ready queue. The active workflow can inspect this mailbox via the ConversationContinuationSource API (peek_next_continuation(), take_next_continuation()). If the app does not consume the item, the processor promotes exactly one pending item back into the ready queue after the current turn completes.
Operations API Surface
The processor exposes REST endpoints that do not require SSE:
/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{op}— app-defined operations called directly by UI widgets.- Admin endpoints for app registration, property management, secrets injection, and cleanup.
These endpoints run inside the processor because they need access to the loaded app singleton and its runtime context. This is what makes app-owned UI surfaces practical: widget UI or main UI can call directly into the live app rather than going through a second app server. Frontends may embed those surfaces, but embedding is a client display choice; KDCube serves the app UI and its APIs.
@api(..., route="public", public_auth="bundle"), proc does not authenticate the public request itself. It forwards the raw HTTP request into the app handler so the app can inspect request: Request and apply its own header, token, signature, or other custom verification logic.Runtime Availability Enforcement
The processor treats the canonical enabled.* app props as live runtime policy, not build-time metadata. On every inbound call it resolves the app-level and resource-level switches from effective app props before dispatching the app method.
- App disabled: operations, widgets, and MCP endpoints return
404; scheduled jobs for that app are not reconciled. - Resource disabled: only that operation, widget, MCP endpoint, or cron job is suppressed.
- No switch or missing prop: the surface stays enabled.
HTTP enforcement lives in the processor integration layer; cron enforcement lives in the app scheduler. This is why an app-props update can change surface availability immediately for new requests without rebuilding or redeploying the app.
Communicator Integration
The processor uses the relay communicator pattern: app events are published to Redis pub/sub channels, ingress subscribes and fans them out over SSE to connected clients. This decouples execution from delivery — the processor never holds SSE connections directly, and horizontal scaling of proc replicas does not affect client connectivity.
Recorded Events and Isolated Tool Handoff
The communicator can also record selected post-firewall envelopes into scoped, bounded buffers. Recording is runtime-local while execution is active, but portable recording scopes are included in COMM_SPEC when the processor launches a platform child runtime.
The normal isolated-tool pattern is: child runtime records, child writes comm_recorded_events.json, host merges the side file into the host communicator, and the host sends the merged batch through the configured sink. This covers TOOL_RUNTIME[tool_id] = "local" as well as Docker/Fargate tool execution when the runtime output directory is returned. Sink callbacks remain host-side unless the child explicitly configures its own sink.
Process Topology
Each Uvicorn worker in chat-proc is an independent queue consumer with its own Redis pool, Postgres pool, app registry, inflight reaper, and cleanup loop. All workers across all replicas compete for the same Redis task queues. There is no sticky worker-to-conversation affinity — any worker can claim any task, and conversation ownership is per-turn, not per-instance.