Skip to content

Kernels

Overview

The kernels feature provides a runtime execution layer for code entities.

It supports:

  • A built-in immutable kernel with id = 0 for direct Fennel evaluation.
  • User-defined process kernels that are launched as subprocesses.
  • ZMQ request/reply execution for subprocess kernels.
  • Multiple live instances per process kernel.
  • Graph-native management (list/detail/instance nodes + views).

The runtime is exposed as app.kernels.

Current Status

As of February 20, 2026, the kernels feature is implemented and integrated.

Validated:

  • Kernel CRUD semantics (including immutable kernel 0 and generated UUID ids).
  • Async run-code path for internal and process kernels.
  • Subprocess integration against assets/python/subprocess_kernel_launcher.py.
  • Kernel selection behavior with multiple running instances.
  • Graph integration (kernels / kernel / kernel-instance nodes and views).

Latest test outcome:

  • assets/lua/tests/test-kernels.fnl: all tests passing, including subprocess launcher integration.
  • Full suite (make test) passing in the current workspace.

Goals

  • Let code entities execute asynchronously through a selected kernel.
  • Keep kernel definitions persistent and editable.
  • Support multiple running instances of the same kernel.
  • Keep integration graph-first, so kernels are visible and operable through node views.

Non-goals

  • Notebook execution (notebooks currently do not define execution semantics).
  • Streaming incremental output in the code-entity UI (current UX shows final async result/error text).

Runtime Module

Implementation lives in assets/lua/kernels.fnl.

Main responsibilities:

  • Kernel definition persistence/load.
  • Process instance lifecycle management.
  • ZMQ socket creation and polling.
  • Async code dispatch and callback delivery.
  • Per-kernel log writing.

Kernel Definition Model

Definition fields:

  • id: string UUID for user kernels, numeric 0 for internal kernel.
  • name: optional display/lookup name.
  • cmd: process launch command (process kernels only).
  • cwd: optional process working directory.
  • internal: runtime-only flag for kernel 0.

Kernel 0 is immutable:

  • Cannot be edited.
  • Cannot be deleted.
  • Cannot spawn process instances.

User kernel IDs are always generated by the runtime:

  • Callers cannot provide custom IDs when creating kernels.

Persistence

Kernel definition directory:

  • user-data-dir/space/kernels

File format:

  • One JSON file per kernel.
  • Filename: <id>.json
  • Stored fields: id, name, cmd, cwd

Runtime sidecar directories:

  • user-data-dir/space/kernels/runtime for connection files.
  • user-data-dir/space/kernels/logs for kernel log files.

Per-kernel log path:

  • user-data-dir/space/kernels/logs/<kernel-id>.log

Instance Model

Each process-kernel run creates a distinct instance with its own:

  • Instance id (UUID).
  • Process id (spawned via process.spawn).
  • Connection file path.
  • ZMQ context/socket.
  • Request queue.
  • Status/error metadata.

Statuses:

  • starting
  • running
  • stopping
  • stopped
  • error

Process + ZMQ Protocol

Process launch:

  • Uses process.spawn with args ["/bin/bash", "-lc", <cmd>].
  • Injects KERNEL_CONNECTION_FILE in env.
  • Optional cwd is honored.

Connection bootstrap:

  • The launched kernel writes an endpoint JSON to the connection file.
  • Runtime polls until endpoint appears (startup timeout enforced).
  • Runtime opens ZMQ REQ and connects to endpoint.

Request payload (JSON string sent over ZMQ):

  • code: source string.
  • registers: JSON-encoded register table.
  • catch_errors: boolean.

Response payload (JSON string from subprocess kernel):

  • output: stdout payload.
  • error: error payload.
  • registers: JSON-encoded register table.

Stop handshake:

  • Runtime sends JSON string "STOP" to subprocess kernel.
  • Process is terminated if it does not exit within the stop deadline.

Instance Selection Policy

When executing code against a process kernel, runtime picks:

  • The most recently created running instance for that kernel.

Algorithm:

  • Filter instances for matching kernel id and status == "running".
  • Select the last one in creation order.
  • If no running instance exists, execution fails with an error.

This policy is intentional and currently documented behavior.

Code Entity Integration

Code entity store (assets/lua/entities/code.fnl) now persists:

  • kernel field, defaulting to 0.

kernel is a single selector and can be:

  • A kernel id (number or id string), or
  • A kernel name (string).

Code entity node (assets/lua/graph/nodes/code-entity.fnl) supports:

  • Updating kernel selector.
  • Setting kernel by graph selection (exactly one selected kernel node).
  • Async run via app.kernels:run-code.
  • Emitting run-result signal with a single display string.

Code entity view and preview:

  • Both expose a Run action.
  • Both expose a single result/error label.

Graph Integration

New nodes:

  • kernels list node.
  • kernel:<id> detail node.
  • kernel-instance:<instance-id> instance node.

New views:

  • assets/lua/graph/view/views/kernels.fnl
  • assets/lua/graph/view/views/kernel.fnl
  • assets/lua/graph/view/views/kernel-instance.fnl

Key loader registrations in assets/lua/graph/key-loaders.fnl:

  • Exact key: kernels
  • Prefix: kernel:
  • Prefix: kernel-instance:

Start node now includes kernels target.

Launchable added:

  • assets/lua/launchables/kernels.fnl

Internal Kernel (id=0)

Kernel 0 evaluates Fennel source directly using fennel-evaluator.

Behavior:

  • Runs asynchronously via callback scheduling.
  • Returns formatted output on success.
  • Returns formatted error text on failure.
  • No process spawn.
  • No ZMQ/socket usage.

Error Handling

Representative error cases:

  • Unknown kernel id/name.
  • Ambiguous kernel name.
  • Attempted kernel delete while active instances exist.
  • No running instance for selected process kernel.
  • Spawn failures.
  • Startup timeout waiting for connection file.
  • ZMQ send/recv parse errors.
  • Unexpected process exit during startup/run.

Errors are surfaced back to callers as callback payloads and mirrored into kernel logs.

Logging

Per-kernel log includes:

  • Timestamped lifecycle events (spawn/start/connect/exit/stop).
  • Captured subprocess stdout.
  • Captured subprocess stderr.
  • Selected request lifecycle events.

The code-entity UI itself displays only final run text; detailed traces remain in per-kernel log files.

Current Limitations

  • Process-kernel command parsing is shell-based via /bin/bash -lc.
  • No UI-level instance pinning per code entity yet.
  • No round-robin or load-balancing policy across instances.
  • No partial-output streaming in code-entity views.

Testing

Added test module:

  • assets/lua/tests/test-kernels.fnl

Covers:

  • Create/update/delete definitions.
  • Internal kernel immutability.
  • Internal kernel execution callback behavior.
  • Duplicate-name ambiguity errors.

Updated tests:

  • assets/lua/tests/test-code-entities.fnl for kernel persistence/default.
  • assets/lua/tests/test-graph-loaders.fnl for kernels loader coverage.

Suggested Next Validation

Completed:

  • End-to-end subprocess execution validation with assets/python/subprocess_kernel_launcher.py.

Recommended ongoing validation:

  • Stress run with multiple concurrent instances of the same kernel.
  • Longer-running request queues to verify fairness and stability over time.
  • UI-driven regression passes for kernel management views after layout changes.

Possible Improvements

  1. Refactor assets/lua/kernels.fnl into smaller modules. Reason: lifecycle, protocol, persistence, and dispatch logic are currently dense in one file.

  2. Add multi-instance stress and soak tests. Reason: current integration test validates correctness, but not sustained high-load behavior.

  3. Add explicit observability counters for queue depth and per-instance latency. Reason: simplifies diagnosing production/runtime stalls beyond logfile inspection.

  4. Add optional code-entity instance pinning. Reason: current policy intentionally uses the latest running instance; pinning would allow deterministic routing when desired.

See also